From be80288a6e21906e52e04f4c6fcb0d5dfa47a962 Mon Sep 17 00:00:00 2001 From: Jos Rothman Date: Wed, 4 Nov 2026 23:22:19 +0100 Subject: [PATCH] Show the offline screen when the network link is gone; plain panel With no address on any interface (cable out, no wifi) the kiosk now switches to the offline screen right away and returns when the network and server are back. An unreachable server with the network up still leaves the page running for InfoFlow's offline cache. The panel on the local pages loses its blue top edge. Co-Authored-By: Claude Opus 5.5 --- src/voxip_kiosk/agent.py | 29 ++++++++++++++++++++++++----- tests/test_agent.py | 26 +++++++++++++++++++++++++- www/kiosk.css | 4 ++-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/voxip_kiosk/agent.py b/src/voxip_kiosk/agent.py index 8dd6a75..8413008 100644 --- a/src/voxip_kiosk/agent.py +++ b/src/voxip_kiosk/agent.py @@ -40,6 +40,7 @@ PROBE_MAX_AGE_LOADING = 10 PROBE_MAX_AGE_ONLINE = 60 HUNG_RELOAD_AFTER = 3 HUNG_RESTART_AFTER = 6 +NO_NETWORK = "Geen netwerkverbinding (netwerkkabel los?)" BLANK_URLS = {"", "about:blank", "chrome://newtab/", "chrome://new-tab-page/"} @@ -124,6 +125,17 @@ class Agent: log.warning(message) self.state["last_error"] = {"at": int(self.wall()), "message": message} + def has_network(self) -> bool: + """False when no interface has an address, e.g. the cable is out and there is no wifi.""" + return bool(self.addresses()) + + def show_offline(self, config: dict[str, Any], reason: str) -> None: + if self.offline_since is None: + self.offline_since = self.wall() + self.state["offline_since"] = int(self.offline_since) + self.error(f"Kiosk-URL niet bereikbaar ({reason}); offlinepagina getoond") + self.navigate(self.offline_url(config, reason)) + def setup_required(self) -> bool: return self.paths.setup_code_file.exists() and not self.paths.admin_file.exists() @@ -187,6 +199,8 @@ class Agent: if kind != "remote" or url_changed: ok, reason = self.reachable(target, 0 if url_changed else PROBE_MAX_AGE_LOADING) + if not ok and not self.has_network(): + reason = NO_NETWORK if ok: if self.offline_since is not None: log.info("Kiosk-URL weer bereikbaar na %.0f s", self.wall() - self.offline_since) @@ -195,14 +209,19 @@ class Agent: self.offline_since = None self.state["offline_since"] = None elif kind != "offline" or url_changed: - if self.offline_since is None: - self.offline_since = self.wall() - self.state["offline_since"] = int(self.offline_since) - self.error(f"Kiosk-URL niet bereikbaar ({reason}); offlinepagina getoond") - self.navigate(self.offline_url(config, reason)) + self.show_offline(config, reason) self.hung = 0 return + # Without any network the page cannot recover by itself: say so on screen. When only + # the server is unreachable the page stays, so InfoFlow can show its offline cache. + if not self.has_network(): + self.state.update(target_reachable=False, reason=NO_NETWORK, checked_at=int(self.wall())) + self._probe = None + self.hung = 0 + self.show_offline(config, NO_NETWORK) + return + if self.last_load is None: self.last_load = self.clock() diff --git a/tests/test_agent.py b/tests/test_agent.py index 00d997f..1648351 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -100,7 +100,7 @@ def test_chromium_error_page_is_replaced(env): assert "offline.html" in browser.url -def test_running_page_is_left_alone_when_network_drops(env): +def test_running_page_is_left_alone_when_the_server_is_unreachable(env): """Pages with their own offline cache (e.g. InfoFlow displays) must keep running.""" agent, browser, probe, clock, _ = env browser.url = "https://schermen-ijsselflat.voxip.nl" @@ -178,3 +178,27 @@ def test_offline_page_never_receives_the_token(env, paths): agent.tick() assert "offline.html" in browser.url assert "geheim" not in browser.url and "3456" in browser.url + + +def test_offline_page_when_the_network_link_is_gone_then_back(env): + agent, browser, probe, clock, _ = env + network = {"eth0": ["192.168.1.20/24"]} + agent.addresses = lambda: dict(network) + browser.url = "https://schermen-ijsselflat.voxip.nl" + agent.tick() + assert browser.navigations == [] + + network.clear() # cable out + probe.ok = False + clock.t += 5 + agent.tick() + assert "offline.html" in browser.url and "netwerkkabel" in browser.url + clock.t += 5 + agent.tick() + assert len(browser.navigations) == 1 # no re-navigation while it stays down + + network["eth0"] = ["192.168.1.20/24"] # cable back in + probe.ok = True + clock.t += 11 + agent.tick() + assert browser.url == "https://schermen-ijsselflat.voxip.nl" diff --git a/www/kiosk.css b/www/kiosk.css index 642d8fd..6ac7802 100644 --- a/www/kiosk.css +++ b/www/kiosk.css @@ -1,5 +1,5 @@ /* 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. */ + display (dark theme): flat dark grey, white type, plain panels. */ :root { color-scheme: dark; --bg: #111827; @@ -21,7 +21,7 @@ body { font-family: ui-sans-serif, system-ui, "DejaVu Sans", "Liberation Sans", .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; } +.panel { width: min(100%, 1100px); background: var(--panel); 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; }