diff --git a/VERSION b/VERSION index 8e82720..85a5e00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.149 +0.0.150 diff --git a/backend/app/main.py b/backend/app/main.py index 8cf2c55..ce3b4fa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -28,6 +28,13 @@ logging.basicConfig( log = logging.getLogger("nvr") FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend") +# Мобильный dashboard: тот же бэкенд слушает один порт, но в docker проброшен второй +# хост-порт (8443→8090). Запрос на него приходит с Host ".. :8443" → отдаём mobile.html. +MOBILE_PORT = os.environ.get("NVR_MOBILE_PORT", "8443") + + +def _is_mobile(request: Request) -> bool: + return request.headers.get("host", "").endswith(":" + MOBILE_PORT) @asynccontextmanager @@ -109,8 +116,10 @@ app.include_router(ws.router) # ── фронтенд ──────────────────────────────────────────────────── @app.get("/") -async def index() -> FileResponse: - return FileResponse(os.path.join(FRONTEND_DIR, "index.html")) +async def index(request: Request) -> FileResponse: + # на мобильном порту — мобильный dashboard (Live одной камеры + Архив) + page = "mobile.html" if _is_mobile(request) else "index.html" + return FileResponse(os.path.join(FRONTEND_DIR, page)) @app.get("/archive") diff --git a/docker-compose.yml b/docker-compose.yml index 2d035da..c32eaf2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8090", "--ssl-keyfile", "/app/certs/key.pem", "--ssl-certfile", "/app/certs/cert.pem"] ports: - - "443:8090" # дашборд на https://<сервер> + - "443:8090" # десктоп-дашборд на https://<сервер> + - "8443:8090" # мобильный dashboard https://<сервер>:8443 (тот же бэкенд, mobile.html по Host) volumes: - ./certs:/app/certs # self-signed TLS-сертификат (rw: entrypoint генерирует при отсутствии) - ./VERSION:/app/VERSION:ro # номер версии (показывается в UI) diff --git a/frontend/mobile.html b/frontend/mobile.html new file mode 100644 index 0000000..9cc853e --- /dev/null +++ b/frontend/mobile.html @@ -0,0 +1,45 @@ + + + + + + + CoRE.Vizion+ — Mobile + + + +
+
CoRE.Vizion+
+ + +
+
+ +
+
+ +
Нет камер
+
+
+ + + + + +
+ + + diff --git a/frontend/static/mobile.css b/frontend/static/mobile.css new file mode 100644 index 0000000..61dc85c --- /dev/null +++ b/frontend/static/mobile.css @@ -0,0 +1,59 @@ +/* CoRE.Vizion+ — мобильный dashboard (отдельный порт). Тёмная тема, нижние вкладки. */ +* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } +html, body { + margin: 0; height: 100%; background: #0f1115; color: #e6e9ef; + font: 15px/1.4 -apple-system, "Segoe UI", Roboto, Arial, sans-serif; + overflow: hidden; +} +.m-app { display: flex; flex-direction: column; height: 100%; } + +.m-head { + height: 44px; flex: none; display: flex; align-items: center; justify-content: center; + background: #161a22; border-bottom: 1px solid #272d3a; +} +.m-brand { font-weight: 700; font-size: 16px; letter-spacing: .3px; } +.m-brand span { color: #c0392b; } + +.m-view { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } + +.m-bar { + flex: none; display: flex; gap: 8px; padding: 8px; + background: #161a22; border-bottom: 1px solid #272d3a; +} +.m-sel, .m-date { + flex: 1; min-width: 0; background: #1d2230; color: #e6e9ef; border: 1px solid #272d3a; + border-radius: 8px; padding: 10px 12px; font-size: 15px; -webkit-appearance: none; appearance: none; +} +.m-date { flex: 0 0 auto; } + +.m-player { position: relative; background: #000; flex: none; } +.m-player video { width: 100%; aspect-ratio: 16 / 9; display: block; background: #000; object-fit: contain; } +.m-hint { + position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; + color: #8b93a7; font-size: 14px; pointer-events: none; text-align: center; padding: 0 16px; +} + +.m-seglist { flex: 1; min-height: 0; overflow-y: auto; padding: 8px; -webkit-overflow-scrolling: touch; } +.m-seg { + display: flex; align-items: center; justify-content: space-between; width: 100%; text-align: left; + background: #161a22; color: #e6e9ef; border: 1px solid #272d3a; border-radius: 8px; + padding: 13px 14px; margin-bottom: 6px; font-size: 15px; font-variant-numeric: tabular-nums; cursor: pointer; +} +.m-seg:active { background: #1d2230; } +.m-seg.active { background: #c0392b; border-color: #c0392b; color: #fff; } +.m-dur { color: #8b93a7; font-size: 13px; } +.m-seg.active .m-dur { color: #fff; } +.m-empty { color: #8b93a7; text-align: center; padding: 28px 16px; font-size: 14px; } + +.m-tabs { + flex: none; display: flex; background: #161a22; border-top: 1px solid #272d3a; + padding-bottom: env(safe-area-inset-bottom, 0); +} +.m-tab { + flex: 1; background: none; border: 0; color: #8b93a7; cursor: pointer; + padding: 9px 0 11px; font-size: 11px; font-family: inherit; + display: flex; flex-direction: column; align-items: center; gap: 3px; +} +.m-tab .i { font-size: 22px; line-height: 1; } +.m-tab.active { color: #fff; } +.m-tab.active .i { color: #c0392b; } diff --git a/frontend/static/mobile.js b/frontend/static/mobile.js new file mode 100644 index 0000000..9292a3f --- /dev/null +++ b/frontend/static/mobile.js @@ -0,0 +1,116 @@ +// CoRE.Vizion+ — мобильный dashboard. 2 режима: Live (1 камера + выбор) и Архив. +// Видео отдаётся сервером уже перекодированным в H.264 (обычный