Merge branch 'main' of github.com:rvbglas/esp_clock
[esp-clock.git] / data / web / script.js
index 025339e5251315647bbf54428e85f2855360ad04..ee434390e9ecc93aed052d1855a35f37e02cd80c 100644 (file)
@@ -1,61 +1,27 @@
-(function (window, document) {
-
-    // we fetch the elements each time because docusaurus removes the previous
-    // element references on page navigation
-    function getElements() {
-        return {
-            layout: document.getElementById('layout'),
-            menu: document.getElementById('menu'),
-            menuLink: document.getElementById('menuLink')
-        };
-    }
-
-    function toggleClass(element, className) {
-        var classes = element.className.split(/\s+/);
-        var length = classes.length;
-        var i = 0;
-
-        for (; i < length; i++) {
-            if (classes[i] === className) {
-                classes.splice(i, 1);
-                break;
-            }
-        }
-        // The className is not found
-        if (length === classes.length) {
-            classes.push(className);
-        }
-
-        element.className = classes.join(' ');
-    }
-
-    function toggleAll() {
-        var active = 'active';
-        var elements = getElements();
-
-        toggleClass(elements.layout, active);
-        toggleClass(elements.menu, active);
-        toggleClass(elements.menuLink, active);
-    }
-    
-    function handleEvent(e) {
-        var elements = getElements();
-        
-        if (e.target.id === elements.menuLink.id) {
-            toggleAll();
-            e.preventDefault();
-        } else if (elements.menu.className.indexOf('active') !== -1) {
-            toggleAll();
-        }
-    }
-    
-    document.addEventListener('click', handleEvent);
-
-}(this, this.document));
-
 var pages
 var parameters = {}
 var daynames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] 
 var pages
 var parameters = {}
 var daynames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] 
+var msgT
+
+function toggleMenu(e) {
+  active = (document.getElementById('menuLink').className.indexOf('active') !== -1)
+  if (active || e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
+    elements = [ document.getElementById('layout'), document.getElementById('menu'), document.getElementById('menuLink') ]
+    for (const element of elements) {
+      if (!active) {
+        element.classList.add('active')
+      } else {
+        element.classList.remove('active')
+      }
+    }
+    if (e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
+      e.preventDefault()
+    }
+    e.stopPropagation()
+  }
+}
+
+document.getElementById('layout').addEventListener('click', toggleMenu)
 
 function encode(r){
   r = String(r)
 
 function encode(r){
   r = String(r)
@@ -63,22 +29,22 @@ function encode(r){
 }
 
 function getAnchor() {
 }
 
 function getAnchor() {
-    return window.location.hash;
+    return window.location.hash.slice(1);
 }
 
 }
 
-function DrawHeader(project) {
+function drawHeader(project) {
   var menu_header = document.getElementById('_ui_menu_header')
   menu_header.innerHTML = project.name
   document.title = project.name + '/' + project.version
 }
 
   var menu_header = document.getElementById('_ui_menu_header')
   menu_header.innerHTML = project.name
   document.title = project.name + '/' + project.version
 }
 
-function ParseJsonQ(url, callback) {
+function parseJsonQ(url, callback) {
   var req = new XMLHttpRequest();
 
   req.onreadystatechange = function () {
     if (this.readyState != 4) return; 
     if (this.status != 200 && this.status != 500 && this.status != 404) {
   var req = new XMLHttpRequest();
 
   req.onreadystatechange = function () {
     if (this.readyState != 4) return; 
     if (this.status != 200 && this.status != 500 && this.status != 404) {
-      setTimeout(ParseJsonQ(url, callback),30000);
+      setTimeout(parseJsonQ(url, callback),30000);
       return;
     }
     var json = JSON.parse(this.responseText)
       return;
     }
     var json = JSON.parse(this.responseText)
@@ -90,7 +56,7 @@ function ParseJsonQ(url, callback) {
 
 }
 
 
 }
 
-function UpdateElement(id, value) {
+function updateElement(id, value) {
   var element = document.getElementById("_ui_element_"+id)
   if (!element) return;
   var ui_class = element.dataset.ui_class;
   var element = document.getElementById("_ui_element_"+id)
   if (!element) return;
   var ui_class = element.dataset.ui_class;
@@ -122,17 +88,17 @@ function UpdateElement(id, value) {
   }  
 }
 
   }  
 }
 
-function UpdateValues(json) {
+function updateValues(json) {
   for (var key in json) {
     var obj = document.getElementById("_ui_element_"+key)
     if (obj) {
   for (var key in json) {
     var obj = document.getElementById("_ui_element_"+key)
     if (obj) {
-      UpdateElement(key, json[key])
+      updateElement(key, json[key])
     }
     parameters[key] = json[key]
   }
   var notification = document.getElementById('_ui_notification');
   if (parameters['_changed']) {
     }
     parameters[key] = json[key]
   }
   var notification = document.getElementById('_ui_notification');
   if (parameters['_changed']) {
-    notification.innerHTML = '<input style="margin:1em .5em 0;" type="button" id="save" value="Сохранить внесенные изменения" class="pure-button pure-button-primary" onclick="sendAction(\'save\')">'
+    notification.innerHTML = '<input type="button" id="save" value="Сохранить" class="pure-button" onclick="sendAction(\'save\')">'
     notification.removeAttribute('hidden')
   } else {
     notification.innerHTML = ''
     notification.removeAttribute('hidden')
   } else {
     notification.innerHTML = ''
@@ -149,35 +115,39 @@ function sendUpdate(id) {
     case 'number':
     case 'range':
       if (input.checkValidity() && input.value != parameters[id]) {
     case 'number':
     case 'range':
       if (input.checkValidity() && input.value != parameters[id]) {
-        ParseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.value), function(json) {
-          UpdateValues(json)
+        parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.value), function(json) {
+          updateValues(json)
         })
       }
       break
     case 'select':
         })
       }
       break
     case 'select':
-      ParseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.selectedOptions[0].value), function(json) {
-        UpdateValues(json)
+      parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.selectedOptions[0].value), function(json) {
+        updateValues(json)
       })
       break;
     case 'checkbox':
       })
       break;
     case 'checkbox':
-      ParseJsonQ('/config/set?name=' + id + '&value=' + (input.checked?'true':'false'), function(json) {
-        UpdateValues(json)
+      parseJsonQ('/config/set?name=' + id + '&value=' + (input.checked?'true':'false'), function(json) {
+        updateValues(json)
       })      
       break;
     case 'week':        
       })      
       break;
     case 'week':        
-      ParseJsonQ('/config/set?name=' + id + '&value=' + input.dataset.value, function(json) {
-        UpdateValues(json)
+      parseJsonQ('/config/set?name=' + id + '&value=' + input.dataset.value, function(json) {
+        updateValues(json)
       })      
       break;
   }
 }
 
       })      
       break;
   }
 }
 
-function sendAction(name) {
-  ParseJsonQ('/action?name=' + name, function(json) {
+function sendAction(name, params = {}) {
+  var url = '/action?name=' + name
+  for (var param in params) {
+    url += '&'+param+'='+encodeURIComponent(params[param])
+  }
+  parseJsonQ(url, function(json) {
     if (json.result == 'FAILED') {
       alert(json.message)
       if (json.page) {
     if (json.result == 'FAILED') {
       alert(json.message)
       if (json.page) {
-        DrawPage(json.page)
+        drawPage(json.page)
       }
     } else {
       location.reload()
       }
     } else {
       location.reload()
@@ -185,7 +155,7 @@ function sendAction(name) {
   })
 }
 
   })
 }
 
-function ShowPwd(id) {
+function showPwd(id) {
   var x = document.getElementById('_ui_element_' + id)
   if (x.type === "password") {
     x.type = "text";
   var x = document.getElementById('_ui_element_' + id)
   if (x.type === "password") {
     x.type = "text";
@@ -194,20 +164,41 @@ function ShowPwd(id) {
   }
 }
 
   }
 }
 
-function OpenSelect(id) {
+function openSelect(id) {
   var selector = document.getElementById('_ui_elemmodal_'+id);
   selector.removeAttribute("hidden")
 }
 
   var selector = document.getElementById('_ui_elemmodal_'+id);
   selector.removeAttribute("hidden")
 }
 
-function CloseSelect(id) {
+function closeSelect(id) {
   var selector = document.getElementById('_ui_elemmodal_'+id);
   selector.hidden = true
 }
 
   var selector = document.getElementById('_ui_elemmodal_'+id);
   selector.hidden = true
 }
 
-function SelectWiFi(id, ssid) {
-  CloseSelect(id);
+function closeMsg() {
+  document.getElementById("_ui_message").hidden = true;
+}
+
+function fadeMsg() {
+  var msg = document.getElementById('_ui_message');
+  msg.classList.add("fadeout")
+  msgT = setTimeout(()=> { closeMsg() }, 5000);
+}
+
+function openMsg(msgText) {
+  document.getElementById("_ui_message_text").innerText = msgText;
+  document.getElementById("_ui_message").classList.remove("fadeout"); 
+  document.getElementById("_ui_message").removeAttribute('hidden');
+  
+  if (msgT) { 
+    window.clearTimeout(msgT); 
+  }
+  msgT = setTimeout(()=> { fadeMsg(); }, 5000);
+}
+
+function selectWiFi(id, ssid) {
+  closeSelect(id);
   var x = document.getElementById('_ui_element_' + id)
   var x = document.getElementById('_ui_element_' + id)
-  UpdateElement(x,ssid);
+  updateElement(x,ssid);
   sendUpdate(id)
 }
 
   sendUpdate(id)
 }
 
@@ -223,13 +214,13 @@ function getWiFi(id) {
       return;
     }
     var json = JSON.parse(this.responseText)
       return;
     }
     var json = JSON.parse(this.responseText)
-    var table = '<table cellpadding="5" border="0" align="center"><thead class="table-header"><tr><td style="padding: 1rem">SSID</td><td style="padding: 1rem">BSSID</td><td style="padding: 1rem">RSSI</td><td style="padding: 1rem">Канал</td><td style="padding: 1rem">Защита</td></tr></thead></tbody style="border-bottom: lightgrey 1px solid">'
+    var table = '<table cellpadding="5" border="0" align="center"><thead class="table-header"><tr><td>SSID</td><td>BSSID</td><td>RSSI</td><td>Канал</td><td>Защита</td></tr></thead><tbody>'
     if (!json.length) {
       setTimeout(getWiFi(id),5000);
     }
     for (idx in json) {
       var encryption = json[idx].secure == 2? "TKIP" : json[idx].secure == 5? "WEP" : json[idx].secure == 4? "CCMP" : json[idx].secure == 7? "нет" : json[idx].secure == 8? "Автоматически" : "Не определено";
     if (!json.length) {
       setTimeout(getWiFi(id),5000);
     }
     for (idx in json) {
       var encryption = json[idx].secure == 2? "TKIP" : json[idx].secure == 5? "WEP" : json[idx].secure == 4? "CCMP" : json[idx].secure == 7? "нет" : json[idx].secure == 8? "Автоматически" : "Не определено";
-      table += '<tr onclick="SelectWiFi(\''+id+'\',\''+json[idx].ssid+'\')"><td style="padding: 1rem">'+json[idx].ssid+'</td><td style="padding: 1rem">'+json[idx].bssid+'</td><td style="padding: 1rem">'+json[idx].rssi+'</td><td style="padding: 1rem">'+json[idx].channel+'</td><td style="padding: 1rem">'+encryption+'</td></tr>'
+      table += '<tr onclick="selectWiFi(\''+id+'\',\''+json[idx].ssid+'\')"><td>'+json[idx].ssid+'</td><td>'+json[idx].bssid+'</td><td>'+json[idx].rssi+'</td><td>'+json[idx].channel+'</td><td>'+encryption+'</td></tr>'
     }
     
     table += '</tbody></table>'
     }
     
     table += '</tbody></table>'
@@ -238,10 +229,9 @@ function getWiFi(id) {
 
   req.open("GET", "/wifi/scan", true);
   req.send()
 
   req.open("GET", "/wifi/scan", true);
   req.send()
-
 }
 
 }
 
-function ClickDay(id, day) {
+function clickDay(id, day) {
   value = parameters[id].split('')
   day_value = value[day]
   day_value = (day_value=='0')?'1':'0'
   value = parameters[id].split('')
   day_value = value[day]
   day_value = (day_value=='0')?'1':'0'
@@ -252,7 +242,35 @@ function ClickDay(id, day) {
   sendUpdate(id);
 }
 
   sendUpdate(id);
 }
 
-function ElementHTML(element) {
+function sendTime(id) {
+  var value = document.getElementById('_ui_element_' + id).value
+  if (value) {
+    var date = new Date(value)
+    var timestamp = Math.floor(date.getTime()/1000);
+    sendAction('time',{"timestamp":timestamp})
+  }
+}
+
+function downloadFile(url, name) {
+  const a = document.createElement('a')
+  a.href = url
+  a.download = name?name:url.split('/').pop()
+  document.body.appendChild(a)
+  a.click()
+  document.body.removeChild(a)
+}
+
+function uploadConfig() {
+  var elem = document.getElementById('_config_file')
+  if (elem.value) {
+    var data = elem.files[0]
+    console.log(data)
+    fetch('/config/put', {method:'PUT',body:data});
+    elem.value = null
+  }
+}
+
+function elementHTML(element) {
   var value
   if (parameters[element.id] || !isNaN(parameters[element.id])) {
     value = parameters[element.id]
   var value
   if (parameters[element.id] || !isNaN(parameters[element.id])) {
     value = parameters[element.id]
@@ -265,13 +283,13 @@ function ElementHTML(element) {
     case 'hr':
       return '<div class="pure-u-1 pure-u-md-1-3"><hr></div>'
     case 'button':
     case 'hr':
       return '<div class="pure-u-1 pure-u-md-1-3"><hr></div>'
     case 'button':
-      return '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input style="margin:1em .5em 0;" type="button" id="' 
-        + element.id + '" value="' + encode(element.label) + '" class="pure-button pure-button-primary" onclick="sendAction(\'' + element.id + '\')" /></div></div>'
+      return '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="' 
+        + element.id + '" value="' + encode(element.label) + '" class="pure-button" onclick="sendAction(\'' + element.id + '\')" /></div></div>'
     case 'password':
       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
     case 'password':
       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
-        + '<input data-ui_class="password" type="password" id="_ui_element_' + element.id + '" value="' + encode(value) 
-        + '" class="pure-u-1" style="display:inline-block" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
-        + '<div class="hint" onclick="ShowPwd(\'' + element.id + '\')">&#x1F441;</div></div>'
+        + '<div class="hinted"><input data-ui_class="password" type="password" id="_ui_element_' + element.id + '" value="' + encode(value) 
+        + '" class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
+        + '<z class="hint" onclick="showPwd(\'' + element.id + '\')">&#61550;</z></div></div>'
     case 'input':
       var pattern = ""
       if (element.pattern) {
     case 'input':
       var pattern = ""
       if (element.pattern) {
@@ -286,15 +304,15 @@ function ElementHTML(element) {
         pattern = ' pattern="' + encode(element.pattern) + '"'
       }
       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
         pattern = ' pattern="' + encode(element.pattern) + '"'
       }
       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
-        + '<input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
-        + ' class="pure-u-1" style="display:inline-block" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />' 
-        + '<div class="hint" onclick="OpenSelect(\'' + element.id+ '\'); getWiFi(\'' + element.id + '\')">&#128246;</div>'
+        + '<div class="hinted"><input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+        + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />' 
+        + '<z class="hint" onclick="openSelect(\'' + element.id+ '\'); getWiFi(\'' + element.id + '\')">&#61931;</z></div>'
         + '<div class="modal" id="_ui_elemmodal_' + element.id + '" hidden>' 
         + '<div class="modal-content">'
         + '<div id="_ui_elemselect_' + element.id + '"></div>'
         + '<div class="modal" id="_ui_elemmodal_' + element.id + '" hidden>' 
         + '<div class="modal-content">'
         + '<div id="_ui_elemselect_' + element.id + '"></div>'
-        + '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input style="margin:1em .5em 0;" type="button" id="_ui_button_' 
-        + element.id + '" value="Закрыть" class="pure-button pure-button-primary" onclick="CloseSelect(\'' + element.id + '\')"></div>'
-        + '</div></div></div>'
+        + '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="_ui_button_' 
+        + element.id + '" value="Закрыть" class="pure-button" onclick="closeSelect(\'' + element.id + '\')"></div>'
+        + '</div></div></div></div>'
     case 'checkbox':
       return '<div class="pure-u-1 pure-u-md-1-3"><label class="switch socket" for="_ui_element_' + element.id + '">'
         + '<input class="switch" data-ui_class="checkbox" type="checkbox" id="_ui_element_' + element.id + '"' + (parameters[element.id]?' checked':'') + ' onchange="sendUpdate(\'' + element.id + '\')" />'
     case 'checkbox':
       return '<div class="pure-u-1 pure-u-md-1-3"><label class="switch socket" for="_ui_element_' + element.id + '">'
         + '<input class="switch" data-ui_class="checkbox" type="checkbox" id="_ui_element_' + element.id + '"' + (parameters[element.id]?' checked':'') + ' onchange="sendUpdate(\'' + element.id + '\')" />'
@@ -315,13 +333,20 @@ function ElementHTML(element) {
       return options
     case 'week':
       days = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>' 
       return options
     case 'week':
       days = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>' 
-        + '<table data-ui_class="week" data-value="' + value + '" id="_ui_element_' + element.id + '" cellpadding="5" border="0" align="left"><tbody><tr>'
+        + '<table data-ui_class="week" data-value="' + value + '" id="_ui_element_' + element.id + '" cellpadding="5" border="0" class="week"><tbody><tr>'
       for (i=0; i<7; i++) {
         a_enabled = (value[i] == "1")
       for (i=0; i<7; i++) {
         a_enabled = (value[i] == "1")
-        days = days + '<td><div class="weekday' + (a_enabled?"-selected":"") + '" id="_ui_elpart_'+i+'_'+element.id+'" onclick="ClickDay(\'' + element.id + '\', ' + i + ')">'+ daynames[i] + '</div></td>'
+        days = days + '<td><div class="weekday' + (a_enabled?"-selected":"") + '" id="_ui_elpart_'+i+'_'+element.id+'" onclick="clickDay(\'' + element.id + '\', ' + i + ')">'+ daynames[i] + '</div></td>'
       }
       days += '</tr></tbody></table></div>'
       return days
       }
       days += '</tr></tbody></table></div>'
       return days
+    case 'timeset':
+      var now = new Date()
+      now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
+      value = now.toISOString().slice(0, -1);
+      return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+        +'<input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
+        + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div>'
     case 'text':
       return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
     case 'number':
     case 'text':
       return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
     case 'number':
@@ -334,15 +359,22 @@ function ElementHTML(element) {
         + '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
         + '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+    case 'config':
+      return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+        + '<div align="center"><input type="button" class="pure-button row-button" value="Сохранить..." onclick="downloadFile(\'/config/get\',\'config.json\')">'
+        + '<label for="_config_file" class="pure-button row-button">Восстановить...</label>'
+        + '<input type="button" class="pure-button row-button" value="Настройки по умолчанию" onclick="confirm(\'Вы точно хотите сбросить настройки?\')?sendAction(\'reset\'):console.log(\'Не надо так не надо...\')">'
+        + '<input type="file" id="_config_file" onchange="uploadConfig()" style="visibility:hidden">'
+        + '</div></div>'
     case 'table':
     default:
     case 'table':
     default:
-      return '<div class="pure-u-1 pure-u-md-1-3"><table cellpadding="5" border="0" align="center"><tbody><tr><td style="padding-right: 10px; width: 50%;" align="right"><pre>' 
-        + encode(element.label)+ '</pre></td><td style="padding-left: 10px;" id="cT"><pre id="_ui_element_' 
-        + element.id + '" data-ui_class="table" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</pre></td></tr></tbody></table></div>'
+      return '<div class="pure-u-1 pure-u-md-1-3"><table class="texttable" cellpadding="5" border="0" align="center"><tbody><tr><td class="value-name" align="right">' 
+        + encode(element.label)+ '</td><td id="_ui_element_' 
+        + element.id + '" data-ui_class="table" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</td></tr></tbody></table></div>'
   }
 }
 
   }
 }
 
-function DrawPage(id) {
+function drawPage(id) {
   var idx =0, i=0
   for (const page of pages) {
     var menu_link = document.getElementById('_ui_pglink_' + page.id)  
   var idx =0, i=0
   for (const page of pages) {
     var menu_link = document.getElementById('_ui_pglink_' + page.id)  
@@ -359,37 +391,66 @@ function DrawPage(id) {
   var page_content = document.getElementById("_ui_page_content");
   var content = ''
   for (const element of pages[idx].elements) {
   var page_content = document.getElementById("_ui_page_content");
   var content = ''
   for (const element of pages[idx].elements) {
-    content = content + ElementHTML(element)
+    content = content + elementHTML(element) + '\n'
   }
   page_content.innerHTML = content
   }
   page_content.innerHTML = content
-  window.location.hash = '#'+id
+  window.location.hash = id
 }
 
 }
 
-function DrawNavigator(project, pages) {
+function drawNavigator(project, pages) {
   var menu = document.getElementById('_ui_menu_list');
   var list = ''
   for (const page of pages) {
     list = list + '<li id="_ui_pglink_' + page.id 
   var menu = document.getElementById('_ui_menu_list');
   var list = ''
   for (const page of pages) {
     list = list + '<li id="_ui_pglink_' + page.id 
-      + '" class="pure-menu-item"><a class="pure-menu-link" onclick="DrawPage(\''+ page.id +'\')" href="#' + page.id + '">'
+      + '" class="pure-menu-item"><a class="pure-menu-link" onclick="drawPage(\''+ page.id +'\')" href="#' + page.id + '">'
+      + (page.icon?'<span class="icon">'+page.icon+'</span>':'')
       + page.title+'</a></li>'
   }
   menu.innerHTML = list
 }
 
       + page.title+'</a></li>'
   }
   menu.innerHTML = list
 }
 
-function DrawUI(ui) {
-  DrawHeader(ui.project)
+function drawContacts(contacts) {
+  if (!contacts) return;
+  var contact_list = '<hr><h4 class="pure-u1">Контакты</h4>'
+  for (contact of contacts) {
+    const url = new URL(contact)
+    var ref
+    switch (url.protocol) {
+      case 'http':
+      case 'https:':
+        ref = '<span class="icon">&#61461;</span>'+url.hostname
+        break
+      case 'mailto:':
+        ref = '<span class="icon">&#61664;</span>'+url.pathname
+        break
+      case 'tg:':
+        ref = '<span class="icon">&#62150;</span>'+url.pathname
+        contact = 'tg://resolve?domain='+url.pathname
+        break
+      default:
+        ref = '<span class="icon">&#62074;</span>'+url.pathname
+    }
+    contact_list += '<a href="'+contact+'">'+ref+'</a>'
+  }
+  var footer = document.getElementById('_ui_contacts');
+  footer.innerHTML = contact_list
+}
+
+function drawUI(ui) {
+  drawHeader(ui.project)
   pages = ui.pages
   pages = ui.pages
-  DrawNavigator(ui.project, pages)
+  drawNavigator(ui.project, pages)
+  drawContacts(ui.project.contacts)
   var anchor = getAnchor()
   if (anchor) {
   var anchor = getAnchor()
   if (anchor) {
-    DrawPage(anchor)
+    drawPage(anchor)
   } else {
   } else {
-    DrawPage(pages[0].id)
+    drawPage(pages[0].id)
   }
 }
 
 function GetUI() {
   }
 }
 
 function GetUI() {
-  ParseJsonQ("/ui", DrawUI);
+  parseJsonQ("/ui", drawUI);
 }
 
 GetUI()
 }
 
 GetUI()
@@ -397,31 +458,32 @@ GetUI()
 function initES() {
   if (!!window.EventSource) {
     var source = new EventSource('/events');
 function initES() {
   if (!!window.EventSource) {
     var source = new EventSource('/events');
+    openMsg('Соединение установлено')
 
     source.onerror = function(e) {
       if (source.readyState == 2) {
 
     source.onerror = function(e) {
       if (source.readyState == 2) {
+        openMsg('Соединение прервано')
         setTimeout(initES, 5000);
       }
     };
         
         setTimeout(initES, 5000);
       }
     };
         
-    source.addEventListener('keepalive', function(e) {
-      UpdateValues(JSON.parse(e.data));
-    }, false);
-
     source.addEventListener('update', function(e) {
     source.addEventListener('update', function(e) {
-      UpdateValues(JSON.parse(e.data));
+      updateValues(JSON.parse(e.data));
+    }, false);
+    source.addEventListener('message', function(e) {
+      openMsg(e.data);
     }, false);
   }
 }
 
 initES();
 
     }, false);
   }
 }
 
 initES();
 
-function DrawConfig(cfg) {
-  UpdateValues(cfg)
+function drawConfig(cfg) {
+  updateValues(cfg)
 }
 
 function GetCfg() {
 }
 
 function GetCfg() {
-  ParseJsonQ("/config/get", DrawConfig);
+  parseJsonQ("/config/get", drawConfig);
 }
 
 GetCfg()
 }
 
 GetCfg()