De informatie is tijdelijk niet beschikbaar
-Er is op dit moment geen verbinding met de server.
-diff --git a/src/voxip_kiosk/agent.py b/src/voxip_kiosk/agent.py index 4673a50..8dd6a75 100644 --- a/src/voxip_kiosk/agent.py +++ b/src/voxip_kiosk/agent.py @@ -29,7 +29,7 @@ from urllib.parse import urlencode, urlsplit from . import logutil, sysinfo from .cdp import CDP, CDPError -from .config import load_config +from .config import admin_url, load_config from .paths import Paths, load_paths from .validation import mask_url @@ -139,6 +139,7 @@ class Agent: def offline_url(self, config: dict[str, Any], reason: str) -> str: return self.local_url("offline", {"name": config["device_name"], "host": urlsplit(config["url"]).hostname or "", + "url": mask_url(config["url"]), "admin": admin_url(config, self.hostname()), "reason": reason, "since": int(self.offline_since or self.wall())}) def navigate(self, url: str) -> None: diff --git a/src/voxip_kiosk/browser.py b/src/voxip_kiosk/browser.py index 6a7d680..8a8028e 100644 --- a/src/voxip_kiosk/browser.py +++ b/src/voxip_kiosk/browser.py @@ -13,6 +13,7 @@ import logging import os import re import shutil +import socket import subprocess import time from pathlib import Path @@ -20,9 +21,9 @@ from typing import Any from urllib.parse import urlencode from . import logutil, sysinfo -from .config import load_config +from .config import admin_url, load_config from .paths import Paths, load_paths -from .validation import parse_resolution +from .validation import mask_url, parse_resolution log = logging.getLogger("voxip-kiosk-browser") @@ -79,7 +80,8 @@ def build_args(chromium: str, config: dict[str, Any], profile_dir: Path, cache_d def start_page_url(paths: Paths, config: dict[str, Any]) -> str: - query = urlencode({"name": config["device_name"], "next": config["url"]}) + query = urlencode({"name": config["device_name"], "next": config["url"], "url": mask_url(config["url"]), + "admin": admin_url(config, socket.gethostname().split(".")[0])}) return f"file://{paths.www_dir}/start.html?{query}" diff --git a/src/voxip_kiosk/config.py b/src/voxip_kiosk/config.py index 40ab28a..824a683 100644 --- a/src/voxip_kiosk/config.py +++ b/src/voxip_kiosk/config.py @@ -123,6 +123,12 @@ class ConfigError(Exception): self.errors = errors +def admin_url(config: dict[str, Any], hostname: str) -> str: + """Address of the web interface as shown on the kiosk screen.""" + scheme = "https" if config["admin_https"] else "http" + return f"{scheme}://{hostname}.local:{config['admin_port']}" + + def validate(updates: dict[str, Any]) -> dict[str, Any]: """Validate known keys in ``updates``; raise ConfigError listing every problem.""" clean: dict[str, Any] = {} diff --git a/tests/test_agent.py b/tests/test_agent.py index 545afd8..00d997f 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -169,3 +169,12 @@ def test_devtools_down_is_reported(env): browser.down = True agent.tick() assert json.loads(paths.agent_state_file.read_text())["browser"] == "unreachable" + + +def test_offline_page_never_receives_the_token(env, paths): + agent, browser, probe, _, _ = env + save_config(paths.config_file, {"url": "https://e.org/scherm?token=geheim123456"}) + probe.ok, probe.reason = False, "Time-out" + agent.tick() + assert "offline.html" in browser.url + assert "geheim" not in browser.url and "3456" in browser.url diff --git a/tests/test_browser.py b/tests/test_browser.py index 93b27e3..ebffac9 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from urllib.parse import parse_qs, urlsplit from voxip_kiosk import browser as b from voxip_kiosk.config import DEFAULTS @@ -74,9 +75,16 @@ def test_start_page_url_contains_target(paths): assert "next=https%3A%2F%2Fe.org%2Fx%3Ftoken%3Dabc" in url and "name=Hal+1" in url +def test_start_page_shows_only_the_masked_link(paths): + url = b.start_page_url(paths, dict(DEFAULTS, url="https://e.org/x?token=geheim123456")) + query = parse_qs(urlsplit(url).query) + assert query["url"] == ["https://e.org/x?token=…3456"] + assert query["admin"][0].endswith(".local:8080") + + def test_local_pages_exist(): www = Path(__file__).resolve().parents[1] / "www" - for page in ("start.html", "offline.html", "setup.html", "kiosk.css", "logo.svg"): + for page in ("start.html", "offline.html", "setup.html", "kiosk.css", "kiosk.js"): assert (www / page).exists() diff --git a/www/kiosk.css b/www/kiosk.css index 4e64fa6..642d8fd 100644 --- a/www/kiosk.css +++ b/www/kiosk.css @@ -1,26 +1,40 @@ -/* Local pages shown on the kiosk screen itself (file://). Large type, readable from a distance. */ -:root { color-scheme: dark; } +/* Local pages shown on the kiosk screen itself (file://). Same look as the InfoFlow + display (dark theme): flat dark grey, white type, panels with a blue top edge. */ +:root { + color-scheme: dark; + --bg: #111827; + --panel: rgba(0, 0, 0, .2); + --line: rgba(255, 255, 255, .1); + --accent: #2563eb; + --muted: rgba(255, 255, 255, .7); + --faint: rgba(255, 255, 255, .45); +} * { box-sizing: border-box; } html, body { margin: 0; height: 100%; overflow: hidden; cursor: none; user-select: none; } -body { - font-family: system-ui, "DejaVu Sans", "Liberation Sans", Arial, sans-serif; - background: radial-gradient(ellipse at 30% 20%, #163a6b 0%, #0b2545 45%, #061529 100%); - color: #eaf1fb; - display: flex; align-items: center; justify-content: center; -} -.stage { text-align: center; padding: 4vh 6vw; max-width: 1400px; transition: transform 2s ease; } -.logo { width: 9vmin; height: 9vmin; min-width: 64px; min-height: 64px; margin-bottom: 3vh; opacity: .95; } -h1 { font-size: clamp(28px, 5vmin, 72px); margin: 0 0 2vh; font-weight: 700; letter-spacing: -.01em; } -p { font-size: clamp(16px, 2.6vmin, 36px); margin: 0 0 1.5vh; color: #b9cbe4; } -.device { font-size: clamp(14px, 2.2vmin, 28px); color: #7f9cc4; text-transform: uppercase; letter-spacing: .12em; margin-bottom: 2vh; } -.clock { font-size: clamp(48px, 12vmin, 180px); font-weight: 300; font-variant-numeric: tabular-nums; margin: 3vh 0 1vh; color: #fff; } -.date { font-size: clamp(18px, 3vmin, 40px); color: #b9cbe4; } -.status { display: inline-flex; align-items: center; gap: 1.2vmin; margin-top: 4vh; padding: 1.2vmin 2.4vmin; border-radius: 999px; background: rgba(255, 255, 255, .08); font-size: clamp(14px, 2.2vmin, 28px); color: #d6e2f3; } -.dot { width: 1.6vmin; height: 1.6vmin; min-width: 10px; min-height: 10px; border-radius: 50%; background: #f0a020; animation: pulse 1.6s infinite; } -.spinner { width: 7vmin; height: 7vmin; margin: 4vh auto 0; border-radius: 50%; border: .8vmin solid rgba(255, 255, 255, .15); border-top-color: #4f9cf9; animation: spin 1s linear infinite; } -.code { display: inline-block; font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(40px, 10vmin, 150px); font-weight: 700; letter-spacing: .12em; padding: 2vh 4vw; margin: 2vh 0 3vh; border-radius: 2vmin; background: rgba(255, 255, 255, .1); border: 2px solid rgba(79, 156, 249, .6); color: #fff; } -.urls { list-style: none; padding: 0; margin: 1vh 0 3vh; } -.urls li { font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(18px, 3.4vmin, 48px); color: #fff; margin: .8vh 0; } -.hint { font-size: clamp(14px, 2vmin, 26px); color: #7f9cc4; } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes pulse { 50% { opacity: .3; } } +body { font-family: ui-sans-serif, system-ui, "DejaVu Sans", "Liberation Sans", Arial, sans-serif; background: var(--bg); color: #fff; } + +.screen { display: flex; flex-direction: column; height: 100%; padding: 4vmin; transition: transform 2s ease; } +.top { display: flex; justify-content: space-between; align-items: flex-start; gap: 4vmin; } +.name { font-size: clamp(20px, 3.4vmin, 44px); font-weight: 600; } +.when { text-align: right; } +.date { font-size: clamp(18px, 3.6vmin, 44px); color: var(--muted); } +.clock { font-size: clamp(36px, 8vmin, 110px); font-weight: 300; line-height: 1.1; font-variant-numeric: tabular-nums; } + +.middle { flex: 1; display: flex; align-items: center; justify-content: center; padding: 3vmin 0; } +.panel { width: min(100%, 1100px); background: var(--panel); border-top: 4px solid var(--accent); border-radius: 8px; padding: 4.5vmin 5vmin; } +h1 { font-size: clamp(26px, 4.6vmin, 64px); font-weight: 600; margin: 0 0 1.5vmin; } +p { font-size: clamp(16px, 2.6vmin, 34px); color: var(--muted); margin: 0 0 1vmin; } +.link { margin-top: 3.5vmin; padding-top: 2.5vmin; border-top: 1px solid var(--line); font-size: clamp(14px, 2.2vmin, 28px); color: var(--muted); overflow-wrap: anywhere; } +.link .url { font-family: "DejaVu Sans Mono", ui-monospace, monospace; color: #fff; } + +.bottom { display: flex; justify-content: space-between; gap: 4vmin; font-size: clamp(12px, 1.9vmin, 24px); color: var(--faint); } + +.progress { height: 4px; margin-top: 4vmin; background: var(--line); border-radius: 2px; overflow: hidden; } +.progress span { display: block; width: 30%; height: 100%; background: var(--accent); animation: slide 1.8s ease-in-out infinite; } +@keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(340%); } } + +/* setup page */ +.urls { list-style: none; padding: 0; margin: 1.5vmin 0 3vmin; } +.urls li { font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(18px, 3.2vmin, 44px); margin: .8vmin 0; } +.code { display: inline-block; font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(40px, 9vmin, 130px); font-weight: 700; letter-spacing: .1em; padding: 1.5vmin 3vmin; margin: 1.5vmin 0 3vmin; border-radius: 8px; background: rgba(255, 255, 255, .06); border: 1px solid var(--line); } +.hint { font-size: clamp(14px, 2vmin, 26px); color: var(--faint); } diff --git a/www/kiosk.js b/www/kiosk.js new file mode 100644 index 0000000..6487eff --- /dev/null +++ b/www/kiosk.js @@ -0,0 +1,25 @@ +// Shared by the local kiosk pages: device name, clock/date, kiosk link and admin address. +const params = new URLSearchParams(location.search); +const fmtTime = new Intl.DateTimeFormat("nl-NL", { hour: "2-digit", minute: "2-digit" }); +const fmtDate = new Intl.DateTimeFormat("nl-NL", { weekday: "long", day: "numeric", month: "long" }); + +function setText(id, text) { + const el = document.getElementById(id); + if (el) el.textContent = text; +} + +setText("name", params.get("name") || "VoxIP Kiosk"); +// The link arrives with its token already masked (…abcd). +const link = (params.get("url") || "").replace(/^https:\/\//, ""); +if (link) setText("link-url", link); +else document.getElementById("link")?.setAttribute("hidden", ""); +setText("admin", params.get("admin") ? "Beheer: " + params.get("admin") : ""); + +function tickClock() { + const now = new Date(); + setText("clock", fmtTime.format(now)); + const date = fmtDate.format(now); + setText("date", date.charAt(0).toUpperCase() + date.slice(1)); +} +tickClock(); +setInterval(tickClock, 1000); diff --git a/www/logo.svg b/www/logo.svg deleted file mode 100644 index 7ee42a8..0000000 --- a/www/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/www/offline.html b/www/offline.html index 0670f08..cfdf872 100644 --- a/www/offline.html +++ b/www/offline.html @@ -6,50 +6,45 @@
-Er is op dit moment geen verbinding met de server.
-De informatie is tijdelijk niet beschikbaar. Het scherm verbindt automatisch opnieuw.
+ +Open de beheerinterface op een computer, tablet of telefoon in hetzelfde netwerk:
-en voer deze setupcode in om een beheerderswachtwoord in te stellen:
-De code is eenmalig en verdwijnt zodra het wachtwoord is ingesteld.
-Open de beheerinterface op een computer, tablet of telefoon in hetzelfde netwerk:
+en voer deze setupcode in om een beheerderswachtwoord in te stellen:
+De code is eenmalig en verdwijnt zodra het wachtwoord is ingesteld.
+