diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a93d558..ab48895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,4 +22,26 @@ jobs: - name: Run unit tests run: python -m unittest discover -s tests -v - name: Check shell scripts - run: bash -n install.sh configure.sh run_monitor.sh run_dashboard.sh + run: bash -n install.sh configure.sh run_monitor.sh run_dashboard.sh clients/webos/renew-devmode.sh clients/webos/install-renewal-timer.sh + + webos-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "16.20.2" + - name: Install LG webOS CLI + run: npm install -g @webos-tools/cli + - name: Package UplinkWitness TV + run: | + mkdir -p dist/webos + ares-package ./clients/webos + mv com.lucaxtech.app.uplinkwitness_*.ipk dist/webos/ + ls -lh dist/webos/ + - name: Upload webOS IPK + uses: actions/upload-artifact@v4 + with: + name: uplinkwitness-tv-ipk + path: dist/webos/*.ipk + if-no-files-found: error diff --git a/clients/webos/README.md b/clients/webos/README.md new file mode 100644 index 0000000..e04e9c1 --- /dev/null +++ b/clients/webos/README.md @@ -0,0 +1,120 @@ +# UplinkWitness TV for LG webOS + +This directory contains the installable launcher shell for the UplinkWitness TV client. + +Primary physical target for the first validation is the LG OLED55E8PLA / webOS TV 4.x generation. The package deliberately uses a small ES5-compatible launcher and opens the TV interface served by the UplinkWitness host at `/wallboard?webos=1`. + +## Why a hosted UI + +The TV package provides the LG launcher integration, icon, persistent server address and fullscreen app container. The actual dashboard is served by the Raspberry Pi / Linux host. + +This keeps monitoring and presentation in one UplinkWitness version, avoids duplicating the diagnostic engine on the television, avoids cross-origin API plumbing, and allows dashboard improvements to arrive with normal UplinkWitness updates without reinstalling the IPK. + +The universal `/wallboard` browser fallback remains available even when the LG package is not installed. + +## Requirements + +On the TV: + +- LG Developer Mode app installed from the LG Content Store +- Developer Mode enabled +- TV and development computer on the same LAN + +On the development computer: + +- Node.js + npm +- current LG webOS CLI (`@webos-tools/cli`) + +Install the CLI: + + npm install -g @webos-tools/cli + ares -V + +## Package + +From the UplinkWitness repository root: + + ares-package ./clients/webos + +The expected package name begins with: + + com.lucaxtech.app.uplinkwitness_0.1.0_ + +CI also packages the client and publishes an `uplinkwitness-tv-ipk` workflow artifact so the exact tested IPK can be installed without rebuilding it locally. + +## Register the TV + +In the Developer Mode app on the TV, enable Developer Mode and Key Server. Note the TV LAN IP and the six-character passphrase shown by the app. + +Then on the development computer: + + ares-setup-device --add livingroom-tv -i "host=TV_IP" -i "port=9922" -i "username=prisoner" + ares-novacom --device livingroom-tv --getkey + +Enter the passphrase shown on the TV when requested. + +Check connectivity: + + ares-setup-device --list + +## Install and launch + +Replace the package filename with the one produced by `ares-package` or downloaded from CI: + + ares-install --device livingroom-tv ./com.lucaxtech.app.uplinkwitness_0.1.0_all.ipk + ares-launch --device livingroom-tv com.lucaxtech.app.uplinkwitness + +The first launch asks for the UplinkWitness server URL. Enter the address shown by `install.sh` on the monitored host, for example: + + http://your-host.local:8080 + +If mDNS is not resolved by the TV, use the host LAN address instead: + + http://192.168.x.x:8080 + +The launcher intentionally does not ship with an installation-specific hostname prefilled. The server address is stored locally by the TV app after a successful configuration and survives normal app restarts. + +A small **Server settings** button remains available in the launcher shell for the Magic Remote pointer. The Yellow key is also accepted as a shortcut when the launcher shell itself has keyboard focus; it is not relied on as the only recovery path. + +## TV controls + +Inside the UplinkWitness TV interface: + +- Left / Right: switch Overview, Network, Router, Incidents +- Magic Remote pointer: tabs remain clickable +- Back while inside a secondary section: return to Overview +- Magic Remote `Server settings`: reopen the saved-host configuration + +## Compatibility rules + +The TV UI intentionally avoids: + +- CSS Grid +- JavaScript modules +- arrow functions / modern-only syntax +- external JavaScript or CDN dependencies +- direct monitoring logic on the TV + +The graphics target is 1920x1080. + +## Developer Mode renewal + +LG Developer Mode sessions expire unless they are extended. With the current `@webos-tools/cli`, renewal is performed by launching the Developer Mode app with the extension parameter: + + ares-launch --device livingroom-tv com.palmdts.devmode -p "extend=true" + +This command has been physically validated on the OLED55E8PLA and returned the displayed remaining session to approximately 1000 hours. + +For an always-on Raspberry Pi or Linux host, first register the TV and retrieve its key for the same Linux user that runs UplinkWitness. Then install the included weekly systemd timer: + + ./clients/webos/install-renewal-timer.sh livingroom-tv + +The installer performs one real renewal before installing anything, copies the renewal helper to `/usr/local/lib/uplinkwitness/`, and enables `uplinkwitness-webos-renew.timer`. The timer renews approximately once per week and also schedules an attempt after the Linux host boots. + +Useful checks: + + systemctl list-timers uplinkwitness-webos-renew.timer + sudo systemctl start uplinkwitness-webos-renew.service + journalctl -u uplinkwitness-webos-renew.service + +The timer contains no TV IP address or host-specific UplinkWitness URL. It uses the webOS device alias already registered by `ares-setup-device` for the Linux user. diff --git a/clients/webos/appinfo.json b/clients/webos/appinfo.json new file mode 100644 index 0000000..212bdcf --- /dev/null +++ b/clients/webos/appinfo.json @@ -0,0 +1,15 @@ +{ + "id": "com.lucaxtech.app.uplinkwitness", + "version": "0.1.0", + "vendor": "Luca Serioli", + "type": "web", + "main": "index.html", + "title": "UplinkWitness TV", + "appDescription": "TV client for the UplinkWitness connection black box", + "icon": "icon.png", + "largeIcon": "largeicon.png", + "iconColor": "#111827", + "resolution": "1920x1080", + "handlesRelaunch": true, + "transparent": false +} diff --git a/clients/webos/icon.png b/clients/webos/icon.png new file mode 100644 index 0000000..50fd8cf Binary files /dev/null and b/clients/webos/icon.png differ diff --git a/clients/webos/index.html b/clients/webos/index.html new file mode 100644 index 0000000..f962a98 --- /dev/null +++ b/clients/webos/index.html @@ -0,0 +1,141 @@ + + + + + + UplinkWitness TV + + + + + + +
+
+
UplinkWitness TV
+
Connect this TV to your UplinkWitness host.
+ + +
+
+ + +
+
Use the address shown by UplinkWitness after installation, for example http://your-host.local:8080, or the host LAN IP such as http://192.168.x.x:8080.
Use Server settings with the Magic Remote if you need to change the host later.
+
+
+ +
+
+
UplinkWitness
+
+
Opening dashboard…
+
+
+ + + + diff --git a/clients/webos/install-renewal-timer.sh b/clients/webos/install-renewal-timer.sh new file mode 100755 index 0000000..ded0df9 --- /dev/null +++ b/clients/webos/install-renewal-timer.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(id -u)" -eq 0 ]; then + echo "Run this installer as your normal user, not with sudo." >&2 + exit 1 +fi + +DEVICE="${1:-}" +if [ -z "$DEVICE" ]; then + echo "Usage: $0 DEVICE_NAME" >&2 + echo "Example: $0 livingroom-tv" >&2 + exit 2 +fi + +if ! command -v ares-launch >/dev/null 2>&1; then + echo "ares-launch was not found. Install @webos-tools/cli first." >&2 + exit 127 +fi + +if ! command -v ares-setup-device >/dev/null 2>&1; then + echo "ares-setup-device was not found. Install @webos-tools/cli first." >&2 + exit 127 +fi + +if ! ares-setup-device --list | awk -v device="$DEVICE" 'NR > 2 && $1 == device { found=1 } END { exit !found }'; then + echo "webOS device '$DEVICE' is not registered for this user." >&2 + echo "Register it with ares-setup-device and retrieve its key with ares-novacom --getkey first." >&2 + exit 3 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +USER_NAME="$(id -un)" +USER_HOME="${HOME:?HOME is not set}" +ARES_LAUNCH_BIN="$(command -v ares-launch)" +INSTALL_DIR="/usr/local/lib/uplinkwitness" +SERVICE_NAME="uplinkwitness-webos-renew.service" +TIMER_NAME="uplinkwitness-webos-renew.timer" + +echo "Testing Developer Mode renewal before installing the timer..." +ARES_LAUNCH_BIN="$ARES_LAUNCH_BIN" "$SCRIPT_DIR/renew-devmode.sh" "$DEVICE" + +sudo install -d -m 0755 "$INSTALL_DIR" +sudo install -m 0755 "$SCRIPT_DIR/renew-devmode.sh" "$INSTALL_DIR/renew-webos-devmode" + +sudo tee "/etc/systemd/system/$SERVICE_NAME" >/dev/null </dev/null <&2 + echo "Example: $0 livingroom-tv" >&2 + exit 2 +fi + +ARES_LAUNCH_BIN="${ARES_LAUNCH_BIN:-}" +if [ -z "$ARES_LAUNCH_BIN" ]; then + ARES_LAUNCH_BIN="$(command -v ares-launch || true)" +fi + +if [ -z "$ARES_LAUNCH_BIN" ] || [ ! -x "$ARES_LAUNCH_BIN" ]; then + echo "ares-launch was not found. Install @webos-tools/cli first." >&2 + exit 127 +fi + +echo "Renewing LG webOS Developer Mode session for device '$DEVICE'..." +"$ARES_LAUNCH_BIN" --device "$DEVICE" com.palmdts.devmode -p "extend=true" +echo "Developer Mode renewal request completed for '$DEVICE'." diff --git a/templates/wallboard.html b/templates/wallboard.html index 064ec5d..80651d0 100644 --- a/templates/wallboard.html +++ b/templates/wallboard.html @@ -2,55 +2,170 @@ - -UplinkWitness Wallboard - + +UplinkWitness TV + -
-
UplinkWitness Wallboard
Internet connection black box
-
-
-
Caricamento…
-
-
-
-
Ping Internet
-
-
-
WAN / PPPoE
-
-
-
WAN fisica
-
-
-
Uptime router
-
-
-
Temperatura router
-
-
-
Disponibilità 30 giorni
-
-
-
-
-
Latenza Internet · ultime 24 ore
-
Ultimo problema
Nessuno registrato
-
-
-
+
+
+
UplinkWitness TV
Internet connection black box
+
-
-
+
+ + + +
+
CARICAMENTO…
+
-
-
+
+ +
+
+
Internet ping
-
-
+
WAN / PPPoE
-
-
+
Disponibilità 30 giorni
-
-
+
WAN fisica
-
-
+
Router
-
-
+
Temperatura router
-
-
+
+
+
Latenza Internet · ultime 24 ore
+
Ultimo problema
Nessuno registrato
-
+
+
+ +
+
+
ICMP Internet
-
-
+
TCP connect
-
-
+
IPv6
-
-
+
DNS
-
Probe applicativo
+
HTTP
-
Probe connettività
+
Host link
-
+
Gateway neighbour
-
Linux neighbour / ARP evidence
+
Qualità recente
-
-
+
Modalità
-
Core UplinkWitness
+
+
+ +
+
+
Modello / firmware
-
-
+
Uptime router
-
-
+
Sessione WAN
-
-
+
WAN fisica
-
-
+
Traffico WAN
-
-
+
Temperatura CPU
-
-
+
Ottica fibra RX
-
Solo se fibra attiva sul FRITZ
+
Ottica fibra TX
-
Solo se fibra attiva sul FRITZ
+
Reconnect delay
-
Router boot → WAN session
+
+
+ +
+
Caricamento incidenti…
+
+ + +
+ - + \ No newline at end of file diff --git a/tests/test_v13_dashboard.py b/tests/test_v13_dashboard.py index 3b4b38c..5169f3c 100644 --- a/tests/test_v13_dashboard.py +++ b/tests/test_v13_dashboard.py @@ -39,7 +39,8 @@ def test_wallboard_route_and_branding(self): response = client.get("/wallboard") self.assertEqual(response.status_code, 200) html = response.get_data(as_text=True) - self.assertIn("UplinkWitness Wallboard", html) + self.assertIn("UplinkWitness", html) + self.assertIn("Overview", html) self.assertIn("/api/status", html) self.assertIn("/api/history?hours=24", html) self.assertIn("quality_recent", html) diff --git a/tests/test_webos_client.py b/tests/test_webos_client.py new file mode 100644 index 0000000..d93e8fc --- /dev/null +++ b/tests/test_webos_client.py @@ -0,0 +1,74 @@ +import json +import struct +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WEBOS = ROOT / "clients" / "webos" + + +def png_size(path): + data = path.read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n": + return None + return struct.unpack(">II", data[16:24]) + + +class WebOSTVClientTests(unittest.TestCase): + def test_appinfo_has_required_webos_metadata(self): + info = json.loads((WEBOS / "appinfo.json").read_text(encoding="utf-8")) + self.assertEqual(info["id"], "com.lucaxtech.app.uplinkwitness") + self.assertEqual(info["type"], "web") + self.assertEqual(info["main"], "index.html") + self.assertEqual(info["version"], "0.1.0") + self.assertEqual(info["resolution"], "1920x1080") + self.assertEqual(info["icon"], "icon.png") + self.assertEqual(info["largeIcon"], "largeicon.png") + self.assertTrue(info["handlesRelaunch"]) + + def test_webos_icons_match_lg_test_sizes(self): + self.assertEqual(png_size(WEBOS / "icon.png"), (80, 80)) + self.assertEqual(png_size(WEBOS / "largeicon.png"), (130, 130)) + + def test_launcher_persists_server_and_opens_wallboard(self): + html = (WEBOS / "index.html").read_text(encoding="utf-8") + self.assertIn("uplinkwitness.server", html) + self.assertIn("/wallboard?webos=1", html) + self.assertIn("405", html) # Yellow key + self.assertIn('placeholder="http://your-host.local:8080"', html) + self.assertNotIn("linewatch.local", html) + self.assertNotIn("type=\"module\"", html) + + def test_launcher_accepts_server_launch_parameter(self): + html = (WEBOS / "index.html").read_text(encoding="utf-8") + self.assertIn("webOSLaunch", html) + self.assertIn("webOSRelaunch", html) + self.assertIn("PalmSystem.launchParams", html) + self.assertIn("detail.server", html) + self.assertIn("openServer(detail.server)", html) + + def test_devmode_renewal_uses_current_cli_launch_flow(self): + script = (WEBOS / "renew-devmode.sh").read_text(encoding="utf-8") + installer = (WEBOS / "install-renewal-timer.sh").read_text(encoding="utf-8") + self.assertIn("com.palmdts.devmode", script) + self.assertIn('extend=true', script) + self.assertIn("uplinkwitness-webos-renew.timer", installer) + self.assertIn("OnUnitActiveSec=7d", installer) + self.assertNotIn("192.168.", script + installer) + + def test_tv_wallboard_uses_legacy_safe_surface(self): + html = (ROOT / "templates" / "wallboard.html").read_text(encoding="utf-8") + for marker in ("Overview", "Network", "Router", "Incidents", "/api/status", "/api/history?hours=24", "/api/events?limit=30"): + self.assertIn(marker, html) + self.assertIn("XMLHttpRequest", html) + self.assertIn("webos=1", html) + self.assertIn("html.webos #view-overview", html) + self.assertIn("padding-bottom:18px", html) + self.assertNotIn("display:grid", html) + self.assertNotIn("fetch(", html) + self.assertNotIn("=>", html) + self.assertNotIn("dateStyle", html) + + +if __name__ == "__main__": + unittest.main()