annotate default/static/websocket.js @ 84:b2ef34e602cf

base, js (websocket), css (main style): dynamic pages implementation, first draft: this patch introduces the browser part of dynamic pages. Dynamic pages work by establishing a websocket between server and the current page, if requested by server (which means that needed arguments are present in template). Once the connection is established, the server can, for now, reload the page, append HTML elements, or receive arbitrary data (without reloading the page, in opposition to data post). If connection can't be established, a popup will be displayed and connection will be retried many times after variable timeouts. The browser will finally give up and display an alert to client if the number of retries is too high (20 for now).
author Goffi <goffi@goffi.org>
date Wed, 03 Jan 2018 01:12:16 +0100
parents
children 27d6453a6209
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
84
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
1 /*
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
2 SàT templates: suit of templates for Salut à Toi
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
3 Copyright (C) 2017 Jérôme Poisson (goffi@goffi.org)
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
4
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
5 This program is free software: you can redistribute it and/or modify
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
6 it under the terms of the GNU Affero General Public License as published by
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
7 the Free Software Foundation, either version 3 of the License, or
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
8 (at your option) any later version.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
9
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
10 This program is distributed in the hope that it will be useful,
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
13 GNU Affero General Public License for more details.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
14
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
15 You should have received a copy of the GNU Affero General Public License
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
17 */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
18
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
19 /* websocket handler */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
20
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
21
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
22 //TODO: retry websocket instead of reload
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
23 function WSHandler(url, token, debug=false) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
24 var socket = new WebSocket(url, 'libervia_page_' + token );
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
25 var retried = 0;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
26
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
27 var errorHandler = function(error) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
28 if (retried > 20) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
29 console.error("Too many tries, can't start websocket");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
30 alert("Dynamic connection with server can't be established, please try to reload this page in a while or contact your service administrator");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
31 return;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
32 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
33 var delay = Math.floor((Math.random() * 10) + 1) + 30 * Math.min(retried, 6);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
34 notifyRetry(delay, function() {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
35 retried++;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
36 socket = new WebSocket(url, 'libervia_page_' + token );
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
37 socket.addEventListener('error', errorHandler);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
38 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
39 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
40
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
41 socket.addEventListener('error', errorHandler);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
42
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
43 if (debug) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
44 socket.addEventListener('message', function(event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
45 console.log('WS in <== ', JSON.parse(event.data));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
46 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
47 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
48
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
49 socket.addEventListener('message', function(event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
50 try {data = JSON.parse(event.data);}
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
51 catch (e) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
52 console.warn('invalid websocket message received: %s', e);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
53 return;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
54 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
55 switch (data.type) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
56 case 'reload':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
57 location.reload(true);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
58 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
59 case 'dom':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
60 selected_element = document.body.querySelector(data.selectors);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
61 switch (data.update_type) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
62 case 'append':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
63 var template = document.createElement('template');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
64 template.innerHTML = data.html;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
65 new_element = template.content.firstChild;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
66 selected_element.appendChild(new_element);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
67 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
68 default:
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
69 console.warn('Unknown DOM update type: %s', data.update_type);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
70 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
71 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
72 default:
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
73 console.warn('Unknown data type: %s', data.type);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
74 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
75 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
76
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
77 socket.addEventListener('open', function (event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
78 console.log('Websocket opened');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
79 retried = 0;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
80 }.bind(this));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
81
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
82 this.send = function(data) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
83 if (debug) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
84 console.log('WS out ==> ', data);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
85 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
86 socket.send(JSON.stringify(data));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
87 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
88
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
89 function notifyRetry(timeout, retryCb) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
90 /* Show a notification dialog informing the user that server can't be reach
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
91 * and call retryCb after timeout seconds.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
92 * A "retry now" link allows to retry immediately"
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
93 *
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
94 * @param timeout(int): delay before retrying, in seconds
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
95 * @param retryCb(function): function to call when retrying
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
96 */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
97 var startTime = Date.now() / 1000;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
98 var retryIntervalID;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
99 var notif = document.createElement("div");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
100 notif.setAttribute('class', 'notification retry');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
101 //FIXME: we use English without translation for now, must be changed when we can use gettext in Libervia pages
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
102 notif.innerHTML = "<p>Can't reach the server, retrying in <span id='retry_counter'></span> seconds</p><p><a id='retry_now'>retry now</a></p>";
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
103 document.body.appendChild(notif);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
104 var retryCounter = document.getElementById('retry_counter');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
105 retryCounter.textContent = timeout;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
106
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
107 var retry = function () {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
108 clearInterval(retryIntervalID);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
109 notif.parentNode.removeChild(notif);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
110 retryCb();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
111 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
112
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
113 var updateTimer = function () {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
114 var elapsed = Math.floor(Date.now() / 1000 - startTime);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
115 var remaining = timeout - elapsed;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
116 if (remaining < 0) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
117 retry();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
118 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
119 else {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
120 retryCounter.textContent = remaining;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
121 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
122 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
123
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
124 var retryNow = document.getElementById('retry_now');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
125 retryNow.addEventListener('click', function(){
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
126 retry();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
127 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
128
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
129 retryIntervalID = setInterval(updateTimer, 1000);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
130 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
131 }