lws: wip
This commit is contained in:
parent
70b4a4f044
commit
4215537047
@ -6,13 +6,15 @@ import json
|
|||||||
import re
|
import re
|
||||||
import inverterd
|
import inverterd
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
|
import time
|
||||||
import __py_include
|
import __py_include
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from aiohttp.web import HTTPFound
|
from aiohttp.web import HTTPFound
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from homekit.config import config, AppConfigUnit
|
from homekit.config import config, AppConfigUnit, is_development_mode, Translation
|
||||||
from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string
|
from homekit.camera import IpcamConfig
|
||||||
|
from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string, json_serial
|
||||||
from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
|
from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
|
||||||
from homekit.inverter.config import InverterdConfig
|
from homekit.inverter.config import InverterdConfig
|
||||||
from homekit.relay.sunxi_h3_client import RelayClient
|
from homekit.relay.sunxi_h3_client import RelayClient
|
||||||
@ -43,12 +45,16 @@ STATIC_FILES = [
|
|||||||
|
|
||||||
|
|
||||||
def get_js_link(file, version) -> str:
|
def get_js_link(file, version) -> str:
|
||||||
|
if is_development_mode():
|
||||||
|
version = int(time.time())
|
||||||
if version:
|
if version:
|
||||||
file += f'?version={version}'
|
file += f'?version={version}'
|
||||||
return f'<script src="{config.app_config["assets_public_path"]}/{file}" type="text/javascript"></script>'
|
return f'<script src="{config.app_config["assets_public_path"]}/{file}" type="text/javascript"></script>'
|
||||||
|
|
||||||
|
|
||||||
def get_css_link(file, version) -> str:
|
def get_css_link(file, version) -> str:
|
||||||
|
if is_development_mode():
|
||||||
|
version = int(time.time())
|
||||||
if version:
|
if version:
|
||||||
file += f'?version={version}'
|
file += f'?version={version}'
|
||||||
return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">'
|
return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">'
|
||||||
@ -171,20 +177,22 @@ def get_inverter_data() -> tuple:
|
|||||||
|
|
||||||
|
|
||||||
class WebSite(http.HTTPServer):
|
class WebSite(http.HTTPServer):
|
||||||
_modems_config: ModemsConfig
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self._modems_config = ModemsConfig()
|
|
||||||
|
|
||||||
aiohttp_jinja2.setup(
|
aiohttp_jinja2.setup(
|
||||||
self.app,
|
self.app,
|
||||||
loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')),
|
loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')),
|
||||||
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
||||||
)
|
)
|
||||||
env = aiohttp_jinja2.get_env(self.app)
|
env = aiohttp_jinja2.get_env(self.app)
|
||||||
env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'))
|
|
||||||
|
def filter_lang(key, unit):
|
||||||
|
strings = Translation(unit)
|
||||||
|
return strings.get()[key]
|
||||||
|
|
||||||
|
env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial)
|
||||||
|
env.filters['lang'] = filter_lang
|
||||||
|
|
||||||
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
|
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
|
||||||
|
|
||||||
@ -200,6 +208,8 @@ class WebSite(http.HTTPServer):
|
|||||||
self.get('/sms.cgi', self.sms)
|
self.get('/sms.cgi', self.sms)
|
||||||
self.post('/sms.cgi', self.sms_post)
|
self.post('/sms.cgi', self.sms_post)
|
||||||
|
|
||||||
|
self.get('/cams.cgi', self.cams)
|
||||||
|
|
||||||
async def render_page(self,
|
async def render_page(self,
|
||||||
req: http.Request,
|
req: http.Request,
|
||||||
template_name: str,
|
template_name: str,
|
||||||
@ -220,6 +230,11 @@ class WebSite(http.HTTPServer):
|
|||||||
ctx = {}
|
ctx = {}
|
||||||
for k in 'inverter', 'sensors':
|
for k in 'inverter', 'sensors':
|
||||||
ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
|
ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
|
||||||
|
|
||||||
|
cc = IpcamConfig()
|
||||||
|
ctx['camzones'] = cc['zones'].keys()
|
||||||
|
ctx['allcams'] = cc.get_all_cam_names()
|
||||||
|
|
||||||
return await self.render_page(req, 'index',
|
return await self.render_page(req, 'index',
|
||||||
title="Home web site",
|
title="Home web site",
|
||||||
context=ctx)
|
context=ctx)
|
||||||
@ -227,14 +242,15 @@ class WebSite(http.HTTPServer):
|
|||||||
async def modems(self, req: http.Request):
|
async def modems(self, req: http.Request):
|
||||||
return await self.render_page(req, 'modems',
|
return await self.render_page(req, 'modems',
|
||||||
title='Состояние модемов',
|
title='Состояние модемов',
|
||||||
context=dict(modems=self._modems_config))
|
context=dict(modems=ModemsConfig()))
|
||||||
|
|
||||||
async def modems_ajx(self, req: http.Request):
|
async def modems_ajx(self, req: http.Request):
|
||||||
|
mc = ModemsConfig()
|
||||||
modem = req.query.get('id', None)
|
modem = req.query.get('id', None)
|
||||||
if modem not in self._modems_config.keys():
|
if modem not in mc.keys():
|
||||||
raise ValueError('invalid modem id')
|
raise ValueError('invalid modem id')
|
||||||
|
|
||||||
modem_cfg = self._modems_config.get(modem)
|
modem_cfg = mc.get(modem)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg))
|
modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg))
|
||||||
|
|
||||||
@ -247,10 +263,10 @@ class WebSite(http.HTTPServer):
|
|||||||
|
|
||||||
async def modems_verbose(self, req: http.Request):
|
async def modems_verbose(self, req: http.Request):
|
||||||
modem = req.query.get('id', None)
|
modem = req.query.get('id', None)
|
||||||
if modem not in self._modems_config.keys():
|
if modem not in ModemsConfig().keys():
|
||||||
raise ValueError('invalid modem id')
|
raise ValueError('invalid modem id')
|
||||||
|
|
||||||
modem_cfg = self._modems_config.get(modem)
|
modem_cfg = ModemsConfig().get(modem)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True))
|
signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True))
|
||||||
data = [
|
data = [
|
||||||
@ -261,23 +277,23 @@ class WebSite(http.HTTPServer):
|
|||||||
['Dialup connection', dialup_conn]
|
['Dialup connection', dialup_conn]
|
||||||
]
|
]
|
||||||
|
|
||||||
modem_name = self._modems_config.getfullname(modem)
|
modem_name = ModemsConfig().getfullname(modem)
|
||||||
return await self.render_page(req, 'modem_verbose',
|
return await self.render_page(req, 'modem_verbose',
|
||||||
title=f'Подробная информация о модеме "{modem_name}"',
|
title=f'Подробная информация о модеме "{modem_name}"',
|
||||||
context=dict(data=data, modem_name=modem_name))
|
context=dict(data=data, modem_name=modem_name))
|
||||||
|
|
||||||
async def sms(self, req: http.Request):
|
async def sms(self, req: http.Request):
|
||||||
modem = req.query.get('id', list(self._modems_config.keys())[0])
|
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
||||||
is_outbox = int(req.query.get('outbox', 0)) == 1
|
is_outbox = int(req.query.get('outbox', 0)) == 1
|
||||||
error = req.query.get('error', None)
|
error = req.query.get('error', None)
|
||||||
sent = int(req.query.get('sent', 0)) == 1
|
sent = int(req.query.get('sent', 0)) == 1
|
||||||
|
|
||||||
cl = get_modem_client(self._modems_config[modem])
|
cl = get_modem_client(ModemsConfig()[modem])
|
||||||
messages = cl.sms_list(1, 20, is_outbox)
|
messages = cl.sms_list(1, 20, is_outbox)
|
||||||
return await self.render_page(req, 'sms',
|
return await self.render_page(req, 'sms',
|
||||||
title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})",
|
title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})",
|
||||||
context=dict(
|
context=dict(
|
||||||
modems=self._modems_config,
|
modems=ModemsConfig(),
|
||||||
selected_modem=modem,
|
selected_modem=modem,
|
||||||
is_outbox=is_outbox,
|
is_outbox=is_outbox,
|
||||||
error=error,
|
error=error,
|
||||||
@ -286,7 +302,7 @@ class WebSite(http.HTTPServer):
|
|||||||
))
|
))
|
||||||
|
|
||||||
async def sms_post(self, req: http.Request):
|
async def sms_post(self, req: http.Request):
|
||||||
modem = req.query.get('id', list(self._modems_config.keys())[0])
|
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
||||||
is_outbox = int(req.query.get('outbox', 0)) == 1
|
is_outbox = int(req.query.get('outbox', 0)) == 1
|
||||||
|
|
||||||
fd = await req.post()
|
fd = await req.post()
|
||||||
@ -305,7 +321,7 @@ class WebSite(http.HTTPServer):
|
|||||||
raise HTTPFound(f'{return_url}&error=Неверный+номер')
|
raise HTTPFound(f'{return_url}&error=Неверный+номер')
|
||||||
phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
|
phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
|
||||||
|
|
||||||
cl = get_modem_client(self._modems_config[modem])
|
cl = get_modem_client(ModemsConfig()[modem])
|
||||||
cl.sms_send(phone, text)
|
cl.sms_send(phone, text)
|
||||||
raise HTTPFound(return_url)
|
raise HTTPFound(return_url)
|
||||||
|
|
||||||
@ -346,6 +362,9 @@ class WebSite(http.HTTPServer):
|
|||||||
title='Насос',
|
title='Насос',
|
||||||
context=dict(status=status))
|
context=dict(status=status))
|
||||||
|
|
||||||
|
async def cams(self, req: http.Request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
config.load_app(WebKbnConfig)
|
config.load_app(WebKbnConfig)
|
||||||
|
@ -44,7 +44,7 @@ class IpcamConfig(ConfigUnit):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'areas': {
|
'zones': {
|
||||||
'type': 'dict',
|
'type': 'dict',
|
||||||
'keysrules': {'type': 'string'},
|
'keysrules': {'type': 'string'},
|
||||||
'valuesrules': {
|
'valuesrules': {
|
||||||
@ -114,11 +114,14 @@ class IpcamConfig(ConfigUnit):
|
|||||||
# FIXME
|
# FIXME
|
||||||
def get_all_cam_names(self,
|
def get_all_cam_names(self,
|
||||||
filter_by_server: Optional[str] = None,
|
filter_by_server: Optional[str] = None,
|
||||||
filter_by_disk: Optional[int] = None) -> list[int]:
|
filter_by_disk: Optional[int] = None,
|
||||||
|
only_enabled=True) -> list[int]:
|
||||||
cams = []
|
cams = []
|
||||||
if filter_by_server is not None and filter_by_server not in _lbc:
|
if filter_by_server is not None and filter_by_server not in _lbc:
|
||||||
raise ValueError(f'invalid filter_by_server: {filter_by_server} not found in {_lbc.__class__.__name__}')
|
raise ValueError(f'invalid filter_by_server: {filter_by_server} not found in {_lbc.__class__.__name__}')
|
||||||
for cam, params in self['cameras'].items():
|
for cam, params in self['cameras'].items():
|
||||||
|
if only_enabled and not self.is_camera_enabled(cam):
|
||||||
|
continue
|
||||||
if filter_by_server is None or params['server'] == filter_by_server:
|
if filter_by_server is None or params['server'] == filter_by_server:
|
||||||
if filter_by_disk is None or params['disk'] == filter_by_disk:
|
if filter_by_disk is None or params['disk'] == filter_by_disk:
|
||||||
cams.append(int(cam))
|
cams.append(int(cam))
|
||||||
|
@ -266,14 +266,26 @@ class TranslationUnit(BaseConfigUnit):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
TranslationInstances = {}
|
||||||
|
|
||||||
|
|
||||||
class Translation:
|
class Translation:
|
||||||
LANGUAGES = ('en', 'ru')
|
LANGUAGES = ('en', 'ru')
|
||||||
DEFAULT_LANGUAGE = 'ru'
|
DEFAULT_LANGUAGE = 'ru'
|
||||||
|
|
||||||
_langs: dict[str, TranslationUnit]
|
_langs: dict[str, TranslationUnit]
|
||||||
|
|
||||||
|
# def __init_subclass__(cls, **kwargs):
|
||||||
|
# super().__init_subclass__(**kwargs)
|
||||||
|
# cls._instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
unit = args[0]
|
||||||
|
if unit not in TranslationInstances:
|
||||||
|
TranslationInstances[unit] = super(Translation, cls).__new__(cls)
|
||||||
|
return TranslationInstances[unit]
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
super().__init__()
|
|
||||||
self._langs = {}
|
self._langs = {}
|
||||||
for lang in self.LANGUAGES:
|
for lang in self.LANGUAGES:
|
||||||
for dirname in CONFIG_DIRECTORIES:
|
for dirname in CONFIG_DIRECTORIES:
|
||||||
@ -289,7 +301,7 @@ class Translation:
|
|||||||
if len(diff) > 0:
|
if len(diff) > 0:
|
||||||
raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff))
|
raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff))
|
||||||
|
|
||||||
def get(self, lang: str) -> TranslationUnit:
|
def get(self, lang: str = DEFAULT_LANGUAGE) -> TranslationUnit:
|
||||||
return self._langs[lang]
|
return self._langs[lang]
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import re
|
|||||||
import os
|
import os
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections.abc import KeysView
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
@ -119,6 +119,8 @@ def json_serial(obj):
|
|||||||
return obj.timestamp()
|
return obj.timestamp()
|
||||||
if isinstance(obj, Enum):
|
if isinstance(obj, Enum):
|
||||||
return obj.value
|
return obj.value
|
||||||
|
if isinstance(obj, KeysView):
|
||||||
|
return list(obj)
|
||||||
raise TypeError("Type %s not serializable" % type(obj))
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
|
@ -173,3 +173,40 @@
|
|||||||
.camfeeds.is_mobile > .video-container {
|
.camfeeds.is_mobile > .video-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* index page */
|
||||||
|
.camzones {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 5px -5px 0;
|
||||||
|
}
|
||||||
|
.camzones::after {
|
||||||
|
content: "";
|
||||||
|
flex: 0 0 50%;
|
||||||
|
}
|
||||||
|
a.camzone {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--bs-dark);
|
||||||
|
flex: 0 0 calc(50% - 10px);
|
||||||
|
height: 100px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px;
|
||||||
|
/*border: 1px solid #ccc;*/
|
||||||
|
background: #f0f2f4;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.camzone_text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
(function() {
|
|
||||||
var RE_WHITESPACE = /[\t\r\n\f]/g
|
var RE_WHITESPACE = /[\t\r\n\f]/g
|
||||||
|
|
||||||
window.ajax = {
|
var ajax = {
|
||||||
get: function(url, data) {
|
get: function(url, data) {
|
||||||
if (typeof data == 'object') {
|
if (typeof data == 'object') {
|
||||||
var index = 0;
|
var index = 0;
|
||||||
@ -38,14 +37,15 @@ window.ajax = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.extend = function(a, b) {
|
function extend(a, b) {
|
||||||
return Object.assign(a, b);
|
return Object.assign(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ge = function(id) {
|
function ge(id) {
|
||||||
return document.getElementById(id);
|
return document.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
var ua = navigator.userAgent.toLowerCase();
|
var ua = navigator.userAgent.toLowerCase();
|
||||||
window.browserInfo = {
|
window.browserInfo = {
|
||||||
version: (ua.match(/.+(?:me|ox|on|rv|it|ra|ie)[\/: ]([\d.]+)/) || [0,'0'])[1],
|
version: (ua.match(/.+(?:me|ox|on|rv|it|ra|ie)[\/: ]([\d.]+)/) || [0,'0'])[1],
|
||||||
@ -61,12 +61,13 @@ window.browserInfo = {
|
|||||||
operaMini: /opera mini/i.test(ua),
|
operaMini: /opera mini/i.test(ua),
|
||||||
ios: /iphone|ipod|ipad|watchos/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1),
|
ios: /iphone|ipod|ipad|watchos/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1),
|
||||||
};
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
window.isTouchDevice = function() {
|
function isTouchDevice() {
|
||||||
return 'ontouchstart' in window || navigator.msMaxTouchPoints;
|
return 'ontouchstart' in window || navigator.msMaxTouchPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.hasClass = function(el, name) {
|
function hasClass(el, name) {
|
||||||
if (!el)
|
if (!el)
|
||||||
throw new Error('hasClass: invalid element')
|
throw new Error('hasClass: invalid element')
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ window.hasClass = function(el, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addClass = function(el, name) {
|
function addClass(el, name) {
|
||||||
if (!hasClass(el, name)) {
|
if (!hasClass(el, name)) {
|
||||||
el.className = (el.className ? el.className + ' ' : '') + name;
|
el.className = (el.className ? el.className + ' ' : '') + name;
|
||||||
return true
|
return true
|
||||||
@ -88,6 +89,39 @@ window.addClass = function(el, name) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeClass(el, name) {
|
||||||
|
if (!el)
|
||||||
|
throw new Error('removeClass: invalid element')
|
||||||
|
|
||||||
|
if (Array.isArray(name)) {
|
||||||
|
for (var i = 0; i < name.length; i++)
|
||||||
|
removeClass(el, name[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexInit() {
|
||||||
|
var blocks = ['zones', 'all'];
|
||||||
|
for (var i = 0; i < blocks.length; i++) {
|
||||||
|
var button = ge('cam_'+blocks[i]+'_btn');
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
var selected = e.target.getAttribute('data-id');
|
||||||
|
for (var j = 0; j < blocks.length; j++) {
|
||||||
|
var button = ge('cam_'+blocks[j]+'_btn');
|
||||||
|
var content = ge('cam_'+blocks[j]);
|
||||||
|
if (blocks[j] === selected) {
|
||||||
|
addClass(button, 'active');
|
||||||
|
content.style.display = '';
|
||||||
|
} else {
|
||||||
|
removeClass(button, 'active');
|
||||||
|
content.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.Cameras = {
|
window.Cameras = {
|
||||||
hlsOptions: null,
|
hlsOptions: null,
|
||||||
h265webjsOptions: null,
|
h265webjsOptions: null,
|
||||||
@ -316,7 +350,6 @@ window.Cameras = {
|
|||||||
return video.canPlayType('application/vnd.apple.mpegurl');
|
return video.canPlayType('application/vnd.apple.mpegurl');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
class ModemStatusUpdater {
|
class ModemStatusUpdater {
|
||||||
|
@ -28,12 +28,29 @@
|
|||||||
<li class="list-group-item"><a href="/sensors.cgi">Датчики</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li>
|
<li class="list-group-item"><a href="/sensors.cgi">Датчики</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="mt-4"><a href="/cams/"><b>Все камеры</b></a> (<a href="/cams/?high=1">HQ</a>)</h6>
|
<nav class="mt-4">
|
||||||
<ul class="list-group list-group-flush">
|
<div class="nav nav-tabs" id="nav-tab">
|
||||||
{% for id, name in cameras %}
|
<button class="nav-link active" type="button" id="cam_zones_btn" data-id="zones">По зонам</button>
|
||||||
<li class="list-group-item"><a href="/cams/{{ id }}/">{{ name }}</a> (<a href="/cams/{{ id }}/?high=1">HQ</a>)</li>
|
<button class="nav-link" type="button" id="cam_all_btn" data-id="all">Все камеры</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="camzones" id="cam_zones">
|
||||||
|
{% for zone in camzones %}
|
||||||
|
<a href="/cams.cgi?zone={{ zone }}" class="camzone">
|
||||||
|
<div class="camzone_text">{{ zone|lang('ipcam_zones') }}</div>
|
||||||
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>
|
</div>
|
||||||
|
<ul class="list-group list-group-flush" id="cam_all" style="display: none">
|
||||||
|
{% for id in allcams %}
|
||||||
|
<li class="list-group-item"><a href="/cams.cgi?id={{ id }}">{{ id|lang('ipcam') }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{# <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>#}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
indexInit();
|
||||||
|
{% endblock %}
|
||||||
|
@ -12,5 +12,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
ModemStatus.init({{ modems.getkeys()|tojson }});
|
ModemStatus.init({{ modems.keys()|tojson }});
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user