Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b388a01
Add webOS TV app metadata
LucaXTech Sep 6, 2026
5bc9c17
Refine webOS TV app metadata
LucaXTech Sep 6, 2026
c6d52d3
Add installable webOS TV launcher shell
LucaXTech Sep 6, 2026
8e1d050
Add webOS launcher icons
LucaXTech Sep 6, 2026
1ef02e0
Turn wallboard into a webOS-friendly TV interface
LucaXTech Sep 6, 2026
29d32a6
Document webOS TV packaging and physical deployment
LucaXTech Sep 6, 2026
13cf5b4
Add regression tests for webOS TV client
LucaXTech Sep 6, 2026
d70a031
Keep webOS launcher compatible with Chromium 53 CSS
LucaXTech Sep 6, 2026
0e89e88
Update wallboard regression for TV navigation
LucaXTech Sep 6, 2026
7d166b4
Build webOS IPK in CI
LucaXTech Sep 6, 2026
a75dee7
Make webOS server settings reachable with Magic Remote
LucaXTech Sep 6, 2026
1286e97
Tighten webOS physical test instructions
LucaXTech Sep 6, 2026
426a9f9
fix(webos): remove installation-specific server default
LucaXTech Sep 6, 2026
5e704cd
docs(webos): document host-neutral server configuration
LucaXTech Sep 6, 2026
9c1350a
test(webos): reject installation-specific hostname defaults
LucaXTech Sep 6, 2026
54a6da3
feat(webos): add Developer Mode renewal helper
LucaXTech Sep 6, 2026
f2de7d4
feat(webos): add systemd renewal timer installer
LucaXTech Sep 6, 2026
92fbe24
ci(webos): validate renewal helper scripts
LucaXTech Sep 6, 2026
4175a86
docs(webos): document automatic Developer Mode renewal
LucaXTech Sep 6, 2026
4acd38b
test(webos): cover Developer Mode renewal automation
LucaXTech Sep 6, 2026
55228e9
fix(webos): mark renewal scripts executable
LucaXTech Sep 6, 2026
9f84a50
feat(webos): accept launch-time server configuration
LucaXTech Sep 6, 2026
6ec947e
feat(webos): configure server via launch params
LucaXTech Sep 6, 2026
dd1cecd
test(webos): cover launch-param server setup
LucaXTech Sep 6, 2026
4afdb85
fix(webos): remove tiny overview vertical overflow
LucaXTech Sep 6, 2026
66cd192
test(webos): cover overview overflow guard
LucaXTech Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
120 changes: 120 additions & 0 deletions clients/webos/README.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions clients/webos/appinfo.json
Original file line number Diff line number Diff line change
@@ -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
}
Binary file added clients/webos/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 141 additions & 0 deletions clients/webos/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>UplinkWitness TV</title>
<style>
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#070b14;color:#f7f9fc;font-family:Arial,Helvetica,sans-serif}
*{box-sizing:border-box}
#frame{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;border:0;background:#070b14}
#setup,#loading{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;display:none;align-items:center;justify-content:center;background:#070b14}
.panel{width:840px;padding:54px;border:2px solid #243149;border-radius:28px;background:#111827;box-shadow:0 24px 80px rgba(0,0,0,.45)}
.brand{font-size:56px;font-weight:800;letter-spacing:-1px}.muted{color:#93a4bd}.tag{font-size:26px;margin-top:12px}
label{display:block;font-size:24px;margin-top:44px;margin-bottom:14px;color:#cbd5e1}
input{width:100%;height:74px;border:3px solid #334155;border-radius:16px;background:#0b1220;color:white;font-size:30px;padding:0 22px;outline:none}
input:focus{border-color:#6da8ff}
.buttons{display:flex;margin-top:26px}.buttons button{margin-right:18px}
button{min-width:180px;height:68px;border:3px solid transparent;border-radius:16px;background:#26344d;color:#f7f9fc;font-size:25px;font-weight:700;padding:0 26px}
button:focus{outline:none;border-color:#f7f9fc;background:#31517e}
button.primary{background:#2f69a9}
.hint{font-size:20px;line-height:1.5;margin-top:28px;color:#93a4bd}
.error{min-height:28px;margin-top:18px;color:#ff8c8c;font-size:22px}
#loading .panel{text-align:center;width:680px}.spinner{width:58px;height:58px;border:7px solid #243149;border-top-color:#6da8ff;border-radius:50%;margin:30px auto;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
#settingsButton{position:absolute;z-index:6;left:26px;bottom:22px;min-width:0;width:auto;height:auto;padding:9px 14px;border:1px solid #334155;border-radius:10px;background:rgba(7,11,20,.78);color:#93a4bd;font-size:16px;font-weight:600}
#settingsButton:focus,#settingsButton:hover{color:#f7f9fc;border-color:#6da8ff;background:#111827}
</style>
</head>
<body>
<iframe id="frame" title="UplinkWitness TV"></iframe>
<button id="settingsButton" type="button">Server settings</button>

<div id="setup">
<div class="panel">
<div class="brand">UplinkWitness <span class="muted">TV</span></div>
<div class="tag muted">Connect this TV to your UplinkWitness host.</div>
<label for="server">Server URL</label>
<input id="server" type="url" inputmode="url" autocomplete="off" spellcheck="false" placeholder="http://your-host.local:8080">
<div id="error" class="error"></div>
<div class="buttons">
<button id="connect" class="primary">Connect</button>
<button id="cancel">Cancel</button>
</div>
<div class="hint">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.<br>Use Server settings with the Magic Remote if you need to change the host later.</div>
</div>
</div>

<div id="loading">
<div class="panel">
<div class="brand">UplinkWitness</div>
<div class="spinner"></div>
<div id="loadingText" class="tag muted">Opening dashboard…</div>
</div>
</div>

<script>
(function(){
var KEY='uplinkwitness.server';
var frame=document.getElementById('frame');
var setup=document.getElementById('setup');
var loading=document.getElementById('loading');
var input=document.getElementById('server');
var error=document.getElementById('error');
var cancel=document.getElementById('cancel');
var timer=null;

function cleanUrl(value){
value=(value||'').replace(/^\s+|\s+$/g,'').replace(/\/+$/,'');
if(value && !/^https?:\/\//i.test(value)){value='http://'+value;}
return value;
}
function showSetup(canCancel){
if(timer){clearTimeout(timer);timer=null;}
loading.style.display='none';
setup.style.display='flex';
var saved=localStorage.getItem(KEY);
if(saved){input.value=saved;}
cancel.style.display=canCancel?'inline-block':'none';
error.textContent='';
setTimeout(function(){input.focus();input.select();},100);
}
function showLoading(text){
setup.style.display='none';
document.getElementById('loadingText').textContent=text||'Opening dashboard…';
loading.style.display='flex';
}
function openServer(value){
var server=cleanUrl(value);
if(!server){error.textContent='Enter the UplinkWitness server URL.';return;}
localStorage.setItem(KEY,server);
showLoading('Connecting to '+server+' …');
frame.src=server+'/wallboard?webos=1';
timer=setTimeout(function(){loading.style.display='none';},7000);
}
function activateApp(){
try{
if(window.PalmSystem && PalmSystem.activate){PalmSystem.activate();}
else if(window.webOSSystem && webOSSystem.activate){webOSSystem.activate();}
}catch(e){}
}
function handleLaunch(data){
var detail=data && data.detail ? data.detail : {};
if(detail.server){openServer(detail.server);}
activateApp();
}
function initialLaunchServer(){
try{
if(window.PalmSystem && PalmSystem.launchParams){
var params=JSON.parse(PalmSystem.launchParams);
if(params && params.server){return params.server;}
}
}catch(e){}
return '';
}

document.addEventListener('webOSLaunch',handleLaunch,true);
document.addEventListener('webOSRelaunch',handleLaunch,true);
frame.addEventListener('load',function(){
if(timer){clearTimeout(timer);timer=null;}
loading.style.display='none';
setup.style.display='none';
try{frame.contentWindow.focus();}catch(e){}
});
document.getElementById('connect').addEventListener('click',function(){openServer(input.value);});
document.getElementById('settingsButton').addEventListener('click',function(){showSetup(!!frame.src);});
cancel.addEventListener('click',function(){setup.style.display='none';try{frame.contentWindow.focus();}catch(e){}});
input.addEventListener('keydown',function(e){if((e.keyCode||e.which)===13){openServer(input.value);}});
document.addEventListener('keydown',function(e){
var code=e.keyCode||e.which;
if(code===405 || code===83){e.preventDefault();showSetup(!!frame.src);return;}
if(code===461 && setup.style.display==='flex' && frame.src){e.preventDefault();setup.style.display='none';try{frame.contentWindow.focus();}catch(x){}}
});

var launchServer=initialLaunchServer();
var saved=localStorage.getItem(KEY);
if(launchServer){openServer(launchServer);}
else if(saved){openServer(saved);}
else{showSetup(false);}
})();
</script>
</body>
</html>
83 changes: 83 additions & 0 deletions clients/webos/install-renewal-timer.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
[Unit]
Description=Renew LG webOS Developer Mode session for UplinkWitness
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=$USER_NAME
Environment="HOME=$USER_HOME"
Environment="ARES_LAUNCH_BIN=$ARES_LAUNCH_BIN"
ExecStart=$INSTALL_DIR/renew-webos-devmode $DEVICE
EOF

sudo tee "/etc/systemd/system/$TIMER_NAME" >/dev/null <<EOF
[Unit]
Description=Weekly LG webOS Developer Mode renewal for UplinkWitness

[Timer]
OnBootSec=15min
OnUnitActiveSec=7d
RandomizedDelaySec=30min
Persistent=true
Unit=$SERVICE_NAME

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now "$TIMER_NAME"

echo
echo "Installed $TIMER_NAME for webOS device '$DEVICE'."
echo "The session will be renewed about once a week, with a renewal attempt after host boots."
echo "Check it with: systemctl list-timers $TIMER_NAME"
echo "Run it now with: sudo systemctl start $SERVICE_NAME"
echo "Inspect logs with: journalctl -u $SERVICE_NAME"
Binary file added clients/webos/largeicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading