diff --git a/widgets/.gitignore b/widgets/.gitignore new file mode 100644 index 0000000..3f9851d --- /dev/null +++ b/widgets/.gitignore @@ -0,0 +1,6 @@ +# Manual backup copies — never commit these +*[Cc]opy*.json +* - Copy.json + +# Working drafts — superseded by dated final versions +w0_working.json diff --git a/widgets/README.md b/widgets/README.md index 128a815..868aae9 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -2,56 +2,85 @@ ## Requirements -- **Cockpit native desktop app v1.17.0** (Windows `.exe` installer) -- The BlueOS browser extension version does NOT support DIY widgets -- Download from: https://github.com/bluerobotics/cockpit/releases/tag/v1.17.0 +- **Cockpit native desktop app v1.18.2** (or compatible 1.18.x) +- The BlueOS browser extension ("Lite") does NOT support DIY widgets — the native desktop app is mandatory. +- Cockpit must connect directly to the RPi4/BlueOS IP over plain HTTP (not through an HTTPS reverse proxy). +- BlueOS Pirate Mode must be enabled each session (Settings → General). + +## Network standard + +Use direct IP addresses everywhere. Do NOT use `blueos.local` — mDNS/Avahi resolution is unreliable across subnets and over Tailscale. + +| Host | IP | +|---|---| +| RPi4 (BlueOS gateway) | `192.168.1.100` | +| RPi5 (autonomy brain) | `192.168.1.101` | ## How to install a widget -1. Open the Cockpit native desktop app -2. Connect to the vehicle / VM (`100.84.141.120` for dev VM) -3. Enter edit mode (pencil icon, top right) -4. Click **Add Widget** at the bottom of the screen -5. Scroll right to find the `` DIY widget — drag it onto the view -6. Click the **gear icon** on the widget to open the editor -7. Click **Import** and select the `.json` file for the widget you want -8. The widget will load immediately +1. Open the Cockpit native desktop app. +2. Connect to the vehicle at `192.168.1.100` (RPi4/BlueOS) over HTTP. +3. Enter edit mode (pencil icon, top right). +4. Click **Add Widget** at the bottom of the screen. +5. Scroll right to find the `` DIY widget — drag it onto the view. +6. Click the **gear icon** on the widget to open the editor. +7. Click **Import** and select the `.json` file for the widget. +8. The widget loads immediately. -> **Note:** Widget files are `.json` format (not `.html`). -> Cockpit expects a JSON file with `html`, `css`, and `js` fields. +> Widget files are `.json` (not `.html`). Cockpit expects a JSON file with `html`, `css`, and `js` string fields. + +## Data lake API (Cockpit v1.18.2 DIY widget scope) + +- Read a single live value: `window.cockpit.getDataLakeVariableData(variableId)` — returns the live value. **This is the getter to use.** +- `getAllDataLakeVariablesInfo()` returns metadata only (id/name/type/description), not values. +- `getDataLakeValue()` does NOT exist — do not use. +- `listenToDatalakeVariable()` does NOT exist in DIY widget scope (it is the postMessage iframe External API only). + +## Live data lake variables (published by cockpit_bridge) + +External WebSocket variables carry the `external/` prefix. + +| Variable | Source topic | Notes | +|---|---|---| +| `external/rov-failsafe` | /rov/failsafe | Failsafe state: 0=GREEN 1=AMBER 2=RED | +| `external/rov-ms` | /rov/mission/status | Mission state: 0=IDLE 1=RUNNING 2=PAUSED 3=COMPLETE 4=ABORTED | +| `external/rov-mp` | /rov/mission/status | Mission progress 0.0–1.0 | +| `external/rov-depth` | /rov/depth | Depth (m) | +| `external/rov-heading` | /mavros compass_hdg | Heading (deg) | +| `external/rov-voltage` | /mavros/battery | Battery voltage | ## Widget index -| File | Widget | Notes | +| File | Widget | Status | Reads | +|---|---|---|---| +| w0_data_lake_inspector.json | Data Lake Inspector | Diagnostic. Flashing/scroll issue OUTSTANDING. | All data lake variables | +| w1_system_health_indicator.json | System Health Indicator | CONFIRMED working | `external/rov-failsafe` | +| w2_mission_status.json | Mission Status | CONFIRMED working | `external/rov-ms`, `external/rov-mp` | +| w3_abort_button.json | Abort Button | UI CONFIRMED. Backend pending. | POST → FastAPI `/abort` | +| w4_mission_setup_button.json | Mission Setup Button | UI CONFIRMED. Backend pending. | `external/rov-ms`; opens `/setup` | +| w5_battery_return_budget.json | Return Budget | UI CONFIRMED. Live budget pending. | `SYS_STATUS/battery_remaining` + `external/rov-return-budget` | + +Diagnostic widget (not production): + +| File | Widget | Purpose | |---|---|---| -| w0_data_lake_inspector.json | Data Lake Inspector | Diagnostic — shows all data lake variables. Not production. | -| w1_system_health_indicator.json | System Health Indicator | Traffic light GREEN/AMBER/RED. Reads `rov_failsafe` from data lake. | -| w2_mission_status.json | Mission Status | Progress bar + state. Reads `rov_mission_state` and `rov_mission_progress`. | -| w3_abort_button.json | Abort Button | Confirm dialog → POST /abort to FastAPI. Edit `FASTAPI_HOST` before use. | -| w4_mission_setup_button.json | Mission Setup Button | Opens setup page in new tab. Edit `SETUP_URL` before use. | -| w5_battery_return_budget.json | Return Budget | Headroom warning. Reads `SYS_STATUS/battery_remaining` (native) + `rov_return_budget`. | +| diagnostics/w_probe_api.json | API Probe | Enumerates `window.cockpit.*` methods and live-reads one variable. Used to confirm the data lake getter. Keep for future debugging. | -## Config values to edit before field use +## Backend dependencies (not yet built) -In `w3_abort_button.json` (line 1 of the JS section): -``` -var FASTAPI_HOST = 'http://blueos.local:8081'; -``` +- **FastAPI service on RPi5:8081** — required by W3 (`POST /abort`) and W4 (`GET /setup`). Until built, W3's abort returns a network error (handled cleanly) and W4's setup page is blank. +- **`external/rov-return-budget`** — not yet published by cockpit_bridge. Until then W5 shows the budget as pending and battery as "not reported" under SITL (SITL reports -1). -In `w4_mission_setup_button.json` (line 1 of the JS section): -``` -var SETUP_URL = 'http://blueos.local:8081/setup'; -``` +## Config values -Both values are correct for real hardware. For the dev VM, replace `blueos.local` with the VM Tailscale IP. +W3 and W4 target the RPi5 backend directly: -## Widget data dependencies +- W3 `W3_FASTAPI_HOST` = `http://192.168.1.101:8081` +- W4 `W4_SETUP_URL` = `http://192.168.1.101:8081/setup` -| Widget | Works without backend? | Requires | -|---|---|---| -| W0 Data Lake Inspector | Yes — shows live MAVLink data immediately | Vehicle connected | -| W1 System Health | Shows "waiting" until bridge runs | NAMED_VALUE bridge node | -| W2 Mission Status | Shows "waiting" until bridge runs | NAMED_VALUE bridge node | -| W3 Abort Button | Button visible, POST will fail | FastAPI backend container | -| W4 Setup Button | Button visible, opens 404 | Extension backend serving setup page | -| W5 Return Budget | Battery% shows live, budget shows 0% | FastAPI backend for return budget calc | +## Widget conventions (for new widgets) + +- All element IDs and CSS classes are prefixed per-widget (e.g. `w1-`, `w2-`) to avoid collisions in the shared Cockpit renderer. +- All `document.getElementById` calls must be INSIDE functions, not at script load time (DOM timing). +- Avoid: `body {}` / `:root {}` globals, `@keyframes`, `position: fixed`, Unicode in JS strings, `async/await`. Use `var`, not `let`/`const`. +- A footer status line (debug) is present on data-reading widgets during validation; strip before final production. diff --git a/widgets/diagnostics/w_probe_api.json b/widgets/diagnostics/w_probe_api.json new file mode 100644 index 0000000..9a18cda --- /dev/null +++ b/widgets/diagnostics/w_probe_api.json @@ -0,0 +1,7 @@ +{ + "html": "
heading: waiting...
status: starting
", + + "css": "#probe-root { font-family: monospace; font-size: 11px; color: #00ff00; background: #111; padding: 6px; overflow-y: auto; height: 100%; } #probe-methods { margin-bottom: 6px; border-bottom: 1px solid #333; padding-bottom: 6px; } #probe-heading { font-size: 14px; color: #ffff00; margin-bottom: 4px; } #probe-status { color: #aaa; font-size: 10px; }", + + "js": "// ============================================================\n// Argonaut 3 -- window.cockpit API Probe Widget\n// Purpose: enumerate every method on window.cockpit, then\n// try every plausible data-lake read method for\n// 'external/rov-heading'. Run this once to confirm\n// the correct method name before building W1-W5.\n// Version: 1.0 Date: 2026-06-22\n// Source researched: github.com/bluerobotics/cockpit\n// src/libs/external-api/api.ts (commit 52fb1a2d)\n// The External API uses postMessage (iframes only).\n// DIY widgets run inline -- window.cockpit surface is\n// different and must be probed at runtime.\n// ============================================================\n\n// --- 1. ENUMERATE window.cockpit METHODS ---\n// Run immediately so the method list appears even if\n// data-lake calls fail.\n(function enumerateCockpitMethods() {\n var el = document.getElementById('probe-methods');\n var status = document.getElementById('probe-status');\n if (!el) return;\n\n // Check whether window.cockpit exists at all\n if (typeof window.cockpit === 'undefined') {\n el.innerText = 'ERROR: window.cockpit is undefined';\n if (status) status.innerText = 'status: cockpit object missing -- Pirate Mode enabled?';\n return;\n }\n\n // Collect all own + inherited enumerable keys\n var keys = [];\n for (var k in window.cockpit) {\n try {\n keys.push(k + ' [' + typeof window.cockpit[k] + ']');\n } catch (e) {\n keys.push(k + ' [access error]');\n }\n }\n\n // Also check Object.keys for non-enumerable own props\n var ownKeys = Object.keys(window.cockpit);\n var methodLine = 'window.cockpit methods (' + keys.length + '):\\n' + keys.join('\\n');\n el.innerText = methodLine;\n if (status) status.innerText = 'status: enumerated ' + keys.length + ' keys';\n})();\n\n// --- 2. PROBE DATA-LAKE READ METHODS ---\n// We try each candidate method in sequence.\n// The first one that returns a non-undefined value for\n// 'external/rov-heading' is the correct method for W1-W5.\n\n(function probeDataLake() {\n var headingEl = document.getElementById('probe-heading');\n var statusEl = document.getElementById('probe-status');\n if (!headingEl) return;\n if (typeof window.cockpit === 'undefined') return; // already flagged above\n\n // Candidate method names to probe, in priority order.\n // Based on source research:\n // - getAllDataLakeVariablesInfo() = CONFIRMED metadata-only (id/name/type)\n // - getDataLakeVariableData() = internal Cockpit function (utils-data-lake.ts)\n // - getDataLakeVariableInfo() = paired with above in same file\n // - getDataLakeValue() = mentioned in older docs, not confirmed\n // - listenToDatalakeVariable() = External API (postMessage, for iframes)\n var VARIABLE_ID = 'external/rov-heading';\n var candidates = [\n 'getDataLakeVariableData',\n 'getDataLakeValue',\n 'getDataLakeVariableInfo',\n 'listenToDatalakeVariable'\n ];\n\n // Track which methods exist vs are callable\n var results = [];\n\n // Try synchronous getters first (candidates 0-2)\n var syncCandidates = candidates.slice(0, 3);\n for (var i = 0; i < syncCandidates.length; i++) {\n var name = syncCandidates[i];\n if (typeof window.cockpit[name] !== 'function') {\n results.push(name + ': NOT a function (type=' + typeof window.cockpit[name] + ')');\n continue;\n }\n try {\n var result = window.cockpit[name](VARIABLE_ID);\n if (result !== undefined && result !== null) {\n // Found it -- display the live value\n results.push(name + ': WORKS -> ' + JSON.stringify(result));\n headingEl.innerText = 'heading (' + name + '): ' + JSON.stringify(result);\n statusEl.innerText = 'status: live read confirmed via ' + name;\n } else {\n results.push(name + ': returned undefined/null');\n }\n } catch (e) {\n results.push(name + ': threw -> ' + e.message);\n }\n }\n\n // Try callback-based listenToDatalakeVariable last\n // It registers a callback, not a sync return\n var listenName = 'listenToDatalakeVariable';\n if (typeof window.cockpit[listenName] === 'function') {\n try {\n window.cockpit[listenName](VARIABLE_ID, function(value) {\n var el = document.getElementById('probe-heading');\n if (el) el.innerText = 'heading (listenToDatalakeVariable callback): ' + JSON.stringify(value);\n var sel = document.getElementById('probe-status');\n if (sel) sel.innerText = 'status: live callback from listenToDatalakeVariable';\n });\n results.push(listenName + ': registered callback (watching for value...)');\n } catch (e) {\n results.push(listenName + ': threw -> ' + e.message);\n }\n } else {\n results.push(listenName + ': NOT a function');\n }\n\n // Append probe results under the method list\n var methodsEl = document.getElementById('probe-methods');\n if (methodsEl) {\n methodsEl.innerText += '\\n\\n--- DATA LAKE PROBE ---\\nvar=' + VARIABLE_ID + '\\n' + results.join('\\n');\n }\n})();\n\n// --- 3. LIVE POLL --- \n// If any sync getter worked, re-call it every 2s so we can\n// confirm it tracks live updates (heading should change as\n// the cockpit_bridge publishes new values).\n// We do NOT know which method works yet, so we poll all\n// sync candidates that exist and update the display.\n(function livePoll() {\n var VARIABLE_ID = 'external/rov-heading';\n var syncCandidates = ['getDataLakeVariableData', 'getDataLakeValue', 'getDataLakeVariableInfo'];\n\n // setInterval is allowed in DIY widget JS\n setInterval(function() {\n if (typeof window.cockpit === 'undefined') return;\n for (var i = 0; i < syncCandidates.length; i++) {\n var name = syncCandidates[i];\n if (typeof window.cockpit[name] !== 'function') continue;\n try {\n var val = window.cockpit[name](VARIABLE_ID);\n if (val !== undefined && val !== null) {\n var el = document.getElementById('probe-heading');\n if (el) el.innerText = 'heading (' + name + '): ' + JSON.stringify(val);\n var sel = document.getElementById('probe-status');\n if (sel) sel.innerText = 'status: polling OK via ' + name;\n return; // first working method wins\n }\n } catch (e) { /* silent -- error shown in method list above */ }\n }\n }, 2000);\n})();" +} diff --git a/widgets/w0_data_lake_inspector.html b/widgets/w0_data_lake_inspector.html deleted file mode 100644 index 5ac7b14..0000000 --- a/widgets/w0_data_lake_inspector.html +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - -
- - - - - - - - - - - -
VariableTypeValue
- -
- - - - diff --git a/widgets/w1_system_health_indicator.html b/widgets/w1_system_health_indicator.html deleted file mode 100644 index 82c8b90..0000000 --- a/widgets/w1_system_health_indicator.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - -
- -
-
- Green -
- -
-
- Amber -
- -
-
- Red -
- -
- - -
Connecting…
- - - - - - - diff --git a/widgets/w1_system_health_indicator.json b/widgets/w1_system_health_indicator.json index 61361ea..1bbef8f 100644 --- a/widgets/w1_system_health_indicator.json +++ b/widgets/w1_system_health_indicator.json @@ -1,7 +1,7 @@ { - "html": "
\n
\n
\n Green\n
\n
\n
\n Amber\n
\n
\n
\n Red\n
\n
\n
Connecting...
\n
--
", + "html": "
\n
\n
\n Green\n
\n
\n
\n Amber\n
\n
\n
\n Red\n
\n
\n
Connecting...
\n
--
", - "css": ".circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; }\n.circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; }\n.circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; }\n.circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; }\n.circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; }\n.circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; }\n.circle-label.active { color: #d8eeff; font-weight: bold; }\n#circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; }\n#message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; }\n#message.green { color: #00e09a; }\n#message.amber { color: #ffb830; }\n#message.red { color: #ff3a5a; }\n#footer { margin-top: 6px; font-size: 9px; color: #2a4060; font-family: monospace; }", + "css": ".w1-circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; } .w1-circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; } .w1-circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; } .w1-circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; } .w1-circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; } .w1-circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; } .w1-circle-label.active { color: #d8eeff; font-weight: bold; } #w1-circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; } #w1-message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; } #w1-message.green { color: #00e09a; } #w1-message.amber { color: #ffb830; } #w1-message.red { color: #ff3a5a; } #w1-footer { margin-top: 6px; font-size: 9px; color: #2a4060; font-family: monospace; }", - "js": "var VARIABLE = 'rov_failsa';\nvar POLL_MS = 500;\n\nvar cGreen = document.getElementById('c-green');\nvar cAmber = document.getElementById('c-amber');\nvar cRed = document.getElementById('c-red');\nvar lGreen = document.getElementById('l-green');\nvar lAmber = document.getElementById('l-amber');\nvar lRed = document.getElementById('l-red');\nvar msgEl = document.getElementById('message');\nvar footEl = document.getElementById('footer');\n\nfunction clearAll() {\n cGreen.className = 'circle';\n cAmber.className = 'circle';\n cRed.className = 'circle';\n lGreen.className = 'circle-label';\n lAmber.className = 'circle-label';\n lRed.className = 'circle-label';\n msgEl.className = '';\n}\n\nfunction applyState(state) {\n clearAll();\n if (state === 0) {\n cGreen.className = 'circle green';\n lGreen.className = 'circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n cAmber.className = 'circle amber';\n lAmber.className = 'circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = 'Parameter degraded';\n } else if (state === 2) {\n cRed.className = 'circle red';\n lRed.className = 'circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') {\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeValue(VARIABLE);\n if (raw === null || raw === undefined) {\n applyState(-1);\n footEl.textContent = 'Waiting for ' + VARIABLE;\n return;\n }\n applyState(parseInt(raw, 10));\n footEl.textContent = 'OK';\n } catch(err) {\n footEl.textContent = 'Error';\n }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);" -} \ No newline at end of file + "js": "// W1 -- System Health Indicator\n// Reads external/rov-failsafe from Cockpit data lake.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=GREEN 1=AMBER 2=RED null=waiting\nvar W1_VAR = 'external/rov-failsafe';\nvar W1_POLL_MS = 500;\n\nfunction w1ClearAll() {\n document.getElementById('w1-c-green').className = 'w1-circle';\n document.getElementById('w1-c-amber').className = 'w1-circle';\n document.getElementById('w1-c-red').className = 'w1-circle';\n document.getElementById('w1-l-green').className = 'w1-circle-label';\n document.getElementById('w1-l-amber').className = 'w1-circle-label';\n document.getElementById('w1-l-red').className = 'w1-circle-label';\n document.getElementById('w1-message').className = '';\n}\n\nfunction w1ApplyState(state) {\n w1ClearAll();\n var msgEl = document.getElementById('w1-message');\n if (state === 0) {\n document.getElementById('w1-c-green').className = 'w1-circle green';\n document.getElementById('w1-l-green').className = 'w1-circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n document.getElementById('w1-c-amber').className = 'w1-circle amber';\n document.getElementById('w1-l-amber').className = 'w1-circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = 'Parameter degraded';\n } else if (state === 2) {\n document.getElementById('w1-c-red').className = 'w1-circle red';\n document.getElementById('w1-l-red').className = 'w1-circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction w1Poll() {\n var footEl = document.getElementById('w1-footer');\n try {\n if (typeof window.cockpit === 'undefined') {\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W1_VAR);\n if (raw === null || raw === undefined) {\n w1ApplyState(-1);\n footEl.textContent = 'Waiting for ' + W1_VAR;\n return;\n }\n w1ApplyState(parseInt(raw, 10));\n footEl.textContent = 'OK';\n } catch(err) {\n footEl.textContent = 'Error: ' + err.message;\n }\n}\n\nsetTimeout(w1Poll, 300);\nsetInterval(w1Poll, W1_POLL_MS);" +} diff --git a/widgets/w2_mission_status.html b/widgets/w2_mission_status.html deleted file mode 100644 index 6f5d11f..0000000 --- a/widgets/w2_mission_status.html +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - -
- -
-
-
- -
—%
- -
Waypoint: —
- - - - - - diff --git a/widgets/w2_mission_status.json b/widgets/w2_mission_status.json index fa52ab1..bb500ef 100644 --- a/widgets/w2_mission_status.json +++ b/widgets/w2_mission_status.json @@ -1,7 +1,7 @@ { - "html": "
--
\n
\n
--%
\n
No mission active
\n
Waiting for data
", + "html": "
--
\n
\n
--%
\n
No mission active
\n
Waiting for data
", - "css": "#state-label {\n font-size: 18px;\n font-weight: bold;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n}\n#state-label.idle { color: #6a9bbf; }\n#state-label.running { color: #00e09a; }\n#state-label.paused { color: #ffb830; }\n#state-label.complete { color: #00c8f0; }\n#state-label.aborted { color: #ff3a5a; }\n#bar-wrap {\n width: 100%;\n height: 8px;\n background: #1a2d42;\n border-radius: 4px;\n border: 1px solid #1e3050;\n overflow: hidden;\n margin: 6px 0;\n}\n#bar-fill {\n height: 100%;\n width: 0%;\n border-radius: 4px;\n background: #00e09a;\n transition: width 0.5s ease, background 0.3s;\n}\n#pct-label {\n font-size: 11px;\n color: #6a9bbf;\n text-align: right;\n width: 100%;\n font-family: monospace;\n}\n#waypoint {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n margin-top: 4px;\n}\n#footer {\n font-size: 9px;\n color: #2a4060;\n font-family: monospace;\n margin-top: 4px;\n}", + "css": "#w2-state-label { font-size: 18px; font-weight: bold; letter-spacing: 0.12em; text-transform: uppercase; color: #6a9bbf; text-align: center; font-family: monospace; } #w2-state-label.idle { color: #6a9bbf; } #w2-state-label.running { color: #00e09a; } #w2-state-label.paused { color: #ffb830; } #w2-state-label.complete { color: #00c8f0; } #w2-state-label.aborted { color: #ff3a5a; } #w2-bar-wrap { width: 100%; height: 8px; background: #1a2d42; border-radius: 4px; border: 1px solid #1e3050; overflow: hidden; margin: 6px 0; } #w2-bar-fill { height: 100%; width: 0%; border-radius: 4px; background: #00e09a; transition: width 0.5s ease, background 0.3s; } #w2-pct-label { font-size: 11px; color: #6a9bbf; text-align: right; width: 100%; font-family: monospace; } #w2-waypoint { font-size: 10px; color: #6a9bbf; text-align: center; font-family: monospace; margin-top: 4px; } #w2-footer { font-size: 9px; color: #2a4060; font-family: monospace; margin-top: 4px; }", - "js": "var VAR_STATE = 'external/rov-ms';\nvar VAR_PROGRESS = 'external/rov-mp';\nvar POLL_MS = 500;\n\nvar STATE_NAMES = {0:'IDLE', 1:'RUNNING', 2:'PAUSED', 3:'COMPLETE', 4:'ABORTED'};\nvar STATE_CLASS = {0:'idle', 1:'running', 2:'paused', 3:'complete', 4:'aborted'};\nvar STATE_COLOR = {0:'#1a2d42', 1:'#00e09a', 2:'#ffb830', 3:'#00c8f0', 4:'#ff3a5a'};\n\nvar stateLabelEl = document.getElementById('state-label');\nvar barFillEl = document.getElementById('bar-fill');\nvar pctLabelEl = document.getElementById('pct-label');\nvar waypointEl = document.getElementById('waypoint');\nvar footerEl = document.getElementById('footer');\n\nfunction render(state, progress) {\n var pct = Math.round((progress || 0) * 100);\n stateLabelEl.className = STATE_CLASS[state] || '';\n stateLabelEl.textContent = STATE_NAMES[state] || '?';\n barFillEl.style.width = pct + '%';\n barFillEl.style.background = STATE_COLOR[state] || '#1a2d42';\n pctLabelEl.textContent = (state === 1 || state === 2) ? pct + '%' : (state === 3 ? '100%' : '--%');\n if (state === 1 || state === 2) {\n waypointEl.textContent = 'Progress ' + pct + '% complete';\n } else if (state === 3) {\n waypointEl.textContent = 'Mission complete';\n } else if (state === 4) {\n waypointEl.textContent = 'Mission aborted - vehicle holding';\n } else {\n waypointEl.textContent = 'No mission active';\n }\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') { footerEl.textContent = 'API not ready'; return; }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) { footerEl.textContent = 'Waiting for ' + VAR_STATE; return; }\n var prog = window.cockpit.getDataLakeValue(VAR_PROGRESS);\n render(parseInt(raw, 10), parseFloat(prog || 0));\n } catch(err) { footerEl.textContent = 'Error'; }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);" -} \ No newline at end of file + "js": "// W2 -- Mission Status\n// Reads external/rov-ms (state) and external/rov-mp (progress 0.0-1.0)\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=IDLE 1=RUNNING 2=PAUSED 3=COMPLETE 4=ABORTED\nvar W2_VAR_STATE = 'external/rov-ms';\nvar W2_VAR_PROGRESS = 'external/rov-mp';\nvar W2_POLL_MS = 500;\n\nvar W2_STATE_NAMES = {0:'IDLE', 1:'RUNNING', 2:'PAUSED', 3:'COMPLETE', 4:'ABORTED'};\nvar W2_STATE_CLASS = {0:'idle', 1:'running', 2:'paused', 3:'complete', 4:'aborted'};\nvar W2_STATE_COLOR = {0:'#1a2d42', 1:'#00e09a', 2:'#ffb830', 3:'#00c8f0', 4:'#ff3a5a'};\n\nfunction w2Render(state, progress) {\n var stateLabelEl = document.getElementById('w2-state-label');\n var barFillEl = document.getElementById('w2-bar-fill');\n var pctLabelEl = document.getElementById('w2-pct-label');\n var waypointEl = document.getElementById('w2-waypoint');\n var footerEl = document.getElementById('w2-footer');\n var pct = Math.round((progress || 0) * 100);\n stateLabelEl.className = W2_STATE_CLASS[state] || '';\n stateLabelEl.textContent = W2_STATE_NAMES[state] || '?';\n barFillEl.style.width = pct + '%';\n barFillEl.style.background = W2_STATE_COLOR[state] || '#1a2d42';\n pctLabelEl.textContent = (state === 1 || state === 2) ? pct + '%' : (state === 3 ? '100%' : '--%');\n if (state === 1 || state === 2) {\n waypointEl.textContent = 'Progress ' + pct + '% complete';\n } else if (state === 3) {\n waypointEl.textContent = 'Mission complete';\n } else if (state === 4) {\n waypointEl.textContent = 'Mission aborted - vehicle holding';\n } else {\n waypointEl.textContent = 'No mission active';\n }\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n}\n\nfunction w2Poll() {\n var footerEl = document.getElementById('w2-footer');\n try {\n if (typeof window.cockpit === 'undefined') { footerEl.textContent = 'API not ready'; return; }\n var raw = window.cockpit.getDataLakeVariableData(W2_VAR_STATE);\n if (raw === null || raw === undefined) { footerEl.textContent = 'Waiting for ' + W2_VAR_STATE; return; }\n var prog = window.cockpit.getDataLakeVariableData(W2_VAR_PROGRESS);\n w2Render(parseInt(raw, 10), parseFloat(prog || 0));\n } catch(err) { footerEl.textContent = 'Error: ' + err.message; }\n}\n\nsetTimeout(w2Poll, 300);\nsetInterval(w2Poll, W2_POLL_MS);" +} diff --git a/widgets/w3_abort_button.html b/widgets/w3_abort_button.html deleted file mode 100644 index f6af9be..0000000 --- a/widgets/w3_abort_button.html +++ /dev/null @@ -1,303 +0,0 @@ - - - - - - - - - - -
Standby
- - -
-
-

Abort mission?
Vehicle will hold position
and await instruction.

-
- - -
-
-
-
- - - - diff --git a/widgets/w3_abort_button.json b/widgets/w3_abort_button.json index 6e756e5..48a733b 100644 --- a/widgets/w3_abort_button.json +++ b/widgets/w3_abort_button.json @@ -1,7 +1,7 @@ { - "html": "\n
Standby
\n
\n

Abort mission? Vehicle will hold position.

\n
\n \n \n
\n
\n
", + "html": "\n
Standby
\n
\n

Abort mission? Vehicle will hold position.

\n
\n \n \n
\n
\n
", - "css": "#abort-btn {\n width: 100%;\n padding: 14px 0;\n background: #2a0c18;\n border: 2px solid #ff3a5a;\n border-radius: 6px;\n color: #ff3a5a;\n font-family: monospace;\n font-size: 14px;\n font-weight: bold;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n cursor: pointer;\n}\n#abort-btn:hover { background: #3a1020; box-shadow: 0 0 16px #ff3a5a; }\n#abort-btn:disabled { background: #1a0a10; border-color: #3a1020; color: #3a1020; cursor: not-allowed; }\n#status {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n margin-top: 8px;\n font-family: monospace;\n min-height: 14px;\n}\n#status.ok { color: #00e09a; }\n#status.err { color: #ff3a5a; }\n#confirm-box {\n margin-top: 10px;\n background: #100510;\n border: 2px solid #ff3a5a;\n border-radius: 8px;\n padding: 12px;\n text-align: center;\n box-shadow: 0 0 20px #ff3a5a;\n}\n#confirm-text {\n font-size: 11px;\n color: #d8eeff;\n line-height: 1.5;\n margin-bottom: 10px;\n font-family: monospace;\n}\n#btn-row { display: flex; gap: 8px; justify-content: center; }\n#confirm-yes {\n flex: 1;\n padding: 8px;\n background: #ff3a5a;\n border: none;\n border-radius: 4px;\n color: #fff;\n font-family: monospace;\n font-size: 11px;\n font-weight: bold;\n cursor: pointer;\n}\n#confirm-yes:hover { background: #ff6080; }\n#confirm-no {\n flex: 1;\n padding: 8px;\n background: transparent;\n border: 1px solid #6a9bbf;\n border-radius: 4px;\n color: #6a9bbf;\n font-family: monospace;\n font-size: 11px;\n cursor: pointer;\n}\n#confirm-no:hover { color: #d8eeff; border-color: #d8eeff; }\n#countdown { font-size: 10px; color: #7a1a28; margin-top: 6px; font-family: monospace; }", + "css": "#w3-abort-btn { width: 100%; padding: 14px 0; background: #2a0c18; border: 2px solid #ff3a5a; border-radius: 6px; color: #ff3a5a; font-family: monospace; font-size: 14px; font-weight: bold; letter-spacing: 0.15em; text-transform: uppercase; cursor: pointer; } #w3-abort-btn:hover { background: #3a1020; box-shadow: 0 0 16px #ff3a5a; } #w3-abort-btn:disabled { background: #1a0a10; border-color: #3a1020; color: #3a1020; cursor: not-allowed; } #w3-status { font-size: 10px; color: #6a9bbf; text-align: center; margin-top: 8px; font-family: monospace; min-height: 14px; } #w3-status.ok { color: #00e09a; } #w3-status.err { color: #ff3a5a; } #w3-confirm-box { margin-top: 10px; background: #100510; border: 2px solid #ff3a5a; border-radius: 8px; padding: 12px; text-align: center; box-shadow: 0 0 20px #ff3a5a; } #w3-confirm-text { font-size: 11px; color: #d8eeff; line-height: 1.5; margin-bottom: 10px; font-family: monospace; } #w3-btn-row { display: flex; gap: 8px; justify-content: center; } #w3-confirm-yes { flex: 1; padding: 8px; background: #ff3a5a; border: none; border-radius: 4px; color: #fff; font-family: monospace; font-size: 11px; font-weight: bold; cursor: pointer; } #w3-confirm-yes:hover { background: #ff6080; } #w3-confirm-no { flex: 1; padding: 8px; background: transparent; border: 1px solid #6a9bbf; border-radius: 4px; color: #6a9bbf; font-family: monospace; font-size: 11px; cursor: pointer; } #w3-confirm-no:hover { color: #d8eeff; border-color: #d8eeff; } #w3-countdown { font-size: 10px; color: #7a1a28; margin-top: 6px; font-family: monospace; }", - "js": "var FASTAPI_HOST = 'http://blueos.local:8081';\nvar AUTO_DISMISS_SEC = 10;\n\nvar abortBtn = document.getElementById('abort-btn');\nvar statusEl = document.getElementById('status');\nvar confirmBox = document.getElementById('confirm-box');\nvar confirmYes = document.getElementById('confirm-yes');\nvar confirmNo = document.getElementById('confirm-no');\nvar countdownEl= document.getElementById('countdown');\n\nvar dismissTimer = null;\nvar countdownVal = AUTO_DISMISS_SEC;\n\nfunction showConfirm() {\n confirmBox.style.display = 'block';\n countdownVal = AUTO_DISMISS_SEC;\n countdownEl.textContent = 'Auto-cancel in ' + countdownVal + 's';\n dismissTimer = setInterval(function() {\n countdownVal--;\n countdownEl.textContent = 'Auto-cancel in ' + countdownVal + 's';\n if (countdownVal <= 0) hideConfirm();\n }, 1000);\n}\n\nfunction hideConfirm() {\n confirmBox.style.display = 'none';\n clearInterval(dismissTimer);\n dismissTimer = null;\n countdownEl.textContent = '';\n}\n\nfunction sendAbort() {\n hideConfirm();\n abortBtn.disabled = true;\n statusEl.className = '';\n statusEl.textContent = 'Sending abort...';\n fetch(FASTAPI_HOST + '/abort', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({abort: true})\n })\n .then(function(res) {\n if (res.ok) {\n statusEl.className = 'ok';\n statusEl.textContent = 'Abort sent - vehicle holding';\n } else {\n statusEl.className = 'err';\n statusEl.textContent = 'Server error: ' + res.status;\n abortBtn.disabled = false;\n }\n })\n .catch(function(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'Network error - check connection';\n abortBtn.disabled = false;\n console.error('[Abort]', err);\n });\n}\n\nabortBtn.addEventListener('click', function() {\n if (!abortBtn.disabled) showConfirm();\n});\nconfirmYes.addEventListener('click', sendAbort);\nconfirmNo.addEventListener('click', hideConfirm);" + "js": "// W3 -- Abort Button\n// Confirm dialog with 10s auto-dismiss, then POST to FastAPI /abort\n// No data lake reads needed -- action widget only\n// FASTAPI_HOST: update before field deployment\nvar W3_FASTAPI_HOST = 'http://192.168.1.101:8081';\nvar W3_AUTO_DISMISS_SEC = 10;\nvar w3DismissTimer = null;\nvar w3CountdownVal = W3_AUTO_DISMISS_SEC;\n\nfunction w3ShowConfirm() {\n document.getElementById('w3-confirm-box').style.display = 'block';\n w3CountdownVal = W3_AUTO_DISMISS_SEC;\n document.getElementById('w3-countdown').textContent = 'Auto-cancel in ' + w3CountdownVal + 's';\n w3DismissTimer = setInterval(function() {\n w3CountdownVal--;\n document.getElementById('w3-countdown').textContent = 'Auto-cancel in ' + w3CountdownVal + 's';\n if (w3CountdownVal <= 0) { w3HideConfirm(); }\n }, 1000);\n}\n\nfunction w3HideConfirm() {\n document.getElementById('w3-confirm-box').style.display = 'none';\n clearInterval(w3DismissTimer);\n w3DismissTimer = null;\n document.getElementById('w3-countdown').textContent = '';\n}\n\nfunction w3SendAbort() {\n w3HideConfirm();\n document.getElementById('w3-abort-btn').disabled = true;\n var statusEl = document.getElementById('w3-status');\n statusEl.className = '';\n statusEl.textContent = 'Sending abort...';\n fetch(W3_FASTAPI_HOST + '/abort', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({abort: true})\n })\n .then(function(res) {\n if (res.ok) {\n statusEl.className = 'ok';\n statusEl.textContent = 'Abort sent - vehicle holding';\n } else {\n statusEl.className = 'err';\n statusEl.textContent = 'Server error: ' + res.status;\n document.getElementById('w3-abort-btn').disabled = false;\n }\n })\n .catch(function(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'Network error - check connection';\n document.getElementById('w3-abort-btn').disabled = false;\n });\n}\n\n// Attach event listeners inside setTimeout so DOM is ready\nsetTimeout(function() {\n document.getElementById('w3-abort-btn').addEventListener('click', function() {\n if (!document.getElementById('w3-abort-btn').disabled) { w3ShowConfirm(); }\n });\n document.getElementById('w3-confirm-yes').addEventListener('click', w3SendAbort);\n document.getElementById('w3-confirm-no').addEventListener('click', w3HideConfirm);\n}, 100);" } diff --git a/widgets/w4_mission_setup_button.html b/widgets/w4_mission_setup_button.html deleted file mode 100644 index ace1fcd..0000000 --- a/widgets/w4_mission_setup_button.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - -
- - - - diff --git a/widgets/w4_mission_setup_button.json b/widgets/w4_mission_setup_button.json index b748202..cb68b78 100644 --- a/widgets/w4_mission_setup_button.json +++ b/widgets/w4_mission_setup_button.json @@ -1,7 +1,7 @@ { - "html": "\n
--
", + "html": "\n
--
", - "css": "#setup-btn {\n width: 100%;\n padding: 10px 0;\n background: transparent;\n border: 1px solid #00c8f0;\n border-radius: 5px;\n color: #00c8f0;\n font-family: monospace;\n font-size: 12px;\n font-weight: bold;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: background 0.2s, opacity 0.3s;\n}\n#setup-btn:hover { background: rgba(0,200,240,0.1); }\n#setup-btn.subdued { border-color: #1a2d42; color: #6a9bbf; opacity: 0.5; }\n#setup-btn.subdued:hover { opacity: 0.75; background: transparent; }\n#status {\n font-size: 9px;\n color: #6a9bbf;\n text-align: center;\n margin-top: 6px;\n font-family: monospace;\n}\n#status.warn { color: #ffb830; }", + "css": "#w4-setup-btn { width: 100%; padding: 10px 0; background: transparent; border: 1px solid #00c8f0; border-radius: 5px; color: #00c8f0; font-family: monospace; font-size: 12px; font-weight: bold; letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer; transition: background 0.2s, opacity 0.3s; } #w4-setup-btn:hover { background: rgba(0,200,240,0.1); } #w4-setup-btn.subdued { border-color: #1a2d42; color: #6a9bbf; opacity: 0.5; } #w4-setup-btn.subdued:hover { opacity: 0.75; background: transparent; } #w4-status { font-size: 9px; color: #6a9bbf; text-align: center; margin-top: 6px; font-family: monospace; } #w4-status.warn { color: #ffb830; }", - "js": "var SETUP_URL = 'http://blueos.local:8081/setup';\nvar VAR_STATE = 'rov_mission_state';\nvar POLL_MS = 1000;\n\nvar setupBtn = document.getElementById('setup-btn');\nvar statusEl = document.getElementById('status');\n\nsetupBtn.addEventListener('click', function() {\n window.open(SETUP_URL, '_blank');\n});\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') {\n statusEl.className = '';\n statusEl.textContent = 'Connecting...';\n return;\n }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = 'No mission loaded';\n return;\n }\n var state = parseInt(raw, 10);\n if (state === 1 || state === 2) {\n setupBtn.className = 'subdued';\n statusEl.className = 'warn';\n statusEl.textContent = state === 1 ? 'Mission running' : 'Mission paused';\n } else {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = state === 0 ? 'Configure before diving' : 'Ready for next mission';\n }\n } catch(err) { console.error('[SetupBtn]', err); }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);" + "js": "// W4 -- Mission Setup Button\n// Opens setup page in new tab. Dims when mission is RUNNING or PAUSED.\n// Reads external/rov-ms for mission state.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// SETUP_URL: update before field deployment\nvar W4_SETUP_URL = 'http://192.168.1.101:8081/setup';\nvar W4_VAR_STATE = 'external/rov-ms';\nvar W4_POLL_MS = 1000;\n\nfunction w4Poll() {\n var setupBtn = document.getElementById('w4-setup-btn');\n var statusEl = document.getElementById('w4-status');\n try {\n if (typeof window.cockpit === 'undefined') {\n statusEl.className = '';\n statusEl.textContent = 'Connecting...';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W4_VAR_STATE);\n if (raw === null || raw === undefined) {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = 'No mission loaded';\n return;\n }\n var state = parseInt(raw, 10);\n if (state === 1 || state === 2) {\n setupBtn.className = 'subdued';\n statusEl.className = 'warn';\n statusEl.textContent = state === 1 ? 'Mission running' : 'Mission paused';\n } else {\n setupBtn.className = '';\n statusEl.className = '';\n statusEl.textContent = state === 0 ? 'Configure before diving' : 'Ready for next mission';\n }\n } catch(err) { statusEl.textContent = 'Error'; }\n}\n\n// Attach click listener inside setTimeout so DOM is ready\nsetTimeout(function() {\n document.getElementById('w4-setup-btn').addEventListener('click', function() {\n window.open(W4_SETUP_URL, '_blank');\n });\n}, 100);\n\nsetTimeout(w4Poll, 300);\nsetInterval(w4Poll, W4_POLL_MS);" } diff --git a/widgets/w5_battery_return_budget.html b/widgets/w5_battery_return_budget.html deleted file mode 100644 index 08da13a..0000000 --- a/widgets/w5_battery_return_budget.html +++ /dev/null @@ -1,270 +0,0 @@ - - - - - - - -
-
-
-
-
--%
-
- - -
- Return budget: --% required -
- - -
Headroom: --
- - - - - - - diff --git a/widgets/w5_battery_return_budget.json b/widgets/w5_battery_return_budget.json index da4c570..741cfad 100644 --- a/widgets/w5_battery_return_budget.json +++ b/widgets/w5_battery_return_budget.json @@ -1,7 +1,7 @@ { - "html": "
RETURN BUDGET
\n
\n Battery\n --%\n
\n
\n Required\n --%\n
\n
\n
\n Headroom\n --\n
\n
\n
Waiting for data
\n
--
", + "html": "
RETURN BUDGET
\n
\n Battery\n --%\n
\n
\n Required\n --%\n
\n
\n
\n Headroom\n --\n
\n
\n
Waiting for data
\n
--
", - "css": "#section-label {\n font-size: 9px;\n color: #6a9bbf;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n font-family: monospace;\n width: 100%;\n margin-bottom: 4px;\n}\n.data-row {\n display: flex;\n width: 100%;\n justify-content: space-between;\n align-items: baseline;\n margin: 2px 0;\n}\n.data-label {\n font-size: 10px;\n color: #6a9bbf;\n font-family: monospace;\n}\n.data-value {\n font-size: 16px;\n font-weight: bold;\n color: #d8eeff;\n font-family: monospace;\n min-width: 48px;\n text-align: right;\n}\n.data-value.green { color: #00e09a; }\n.data-value.amber { color: #ffb830; }\n.data-value.red { color: #ff3a5a; }\n#divider {\n width: 100%;\n height: 1px;\n background: #1e3050;\n margin: 4px 0;\n}\n#bar-bg {\n width: 100%;\n height: 5px;\n background: #1a2d42;\n border-radius: 3px;\n overflow: hidden;\n margin: 3px 0;\n}\n#bar-fill {\n height: 100%;\n border-radius: 3px;\n background: #00e09a;\n transition: width 0.5s ease, background 0.3s;\n}\n#status-text {\n font-size: 10px;\n color: #6a9bbf;\n text-align: center;\n font-family: monospace;\n min-height: 12px;\n}\n#status-text.green { color: #00e09a; }\n#status-text.amber { color: #ffb830; }\n#status-text.red { color: #ff3a5a; }\n#footer {\n font-size: 9px;\n color: #1a2d42;\n font-family: monospace;\n margin-top: 2px;\n}", + "css": "#w5-section-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.1em; font-family: monospace; width: 100%; margin-bottom: 4px; } .w5-data-row { display: flex; width: 100%; justify-content: space-between; align-items: baseline; margin: 2px 0; } .w5-data-label { font-size: 10px; color: #6a9bbf; font-family: monospace; } .w5-data-value { font-size: 16px; font-weight: bold; color: #d8eeff; font-family: monospace; min-width: 48px; text-align: right; } .w5-data-value.green { color: #00e09a; } .w5-data-value.amber { color: #ffb830; } .w5-data-value.red { color: #ff3a5a; } #w5-divider { width: 100%; height: 1px; background: #1e3050; margin: 4px 0; } #w5-bar-bg { width: 100%; height: 5px; background: #1a2d42; border-radius: 3px; overflow: hidden; margin: 3px 0; } #w5-bar-fill { height: 100%; border-radius: 3px; background: #00e09a; transition: width 0.5s ease, background 0.3s; } #w5-status-text { font-size: 10px; color: #6a9bbf; text-align: center; font-family: monospace; min-height: 12px; } #w5-status-text.green { color: #00e09a; } #w5-status-text.amber { color: #ffb830; } #w5-status-text.red { color: #ff3a5a; } #w5-footer { font-size: 9px; color: #1a2d42; font-family: monospace; margin-top: 2px; }", - "js": "var VAR_BATTERY = 'SYS_STATUS/battery_remaining';\nvar VAR_BUDGET = 'rov_return_budget';\nvar WARN = 15;\nvar CRIT = 5;\nvar POLL_MS = 1000;\n\nvar battValEl = document.getElementById('batt-val');\nvar budgetValEl = document.getElementById('budget-val');\nvar headroomValEl = document.getElementById('headroom-val');\nvar barFillEl = document.getElementById('bar-fill');\nvar statusTextEl = document.getElementById('status-text');\nvar footerEl = document.getElementById('footer');\n\nfunction setColour(el, sev) {\n el.classList.remove('green', 'amber', 'red');\n if (sev) el.classList.add(sev);\n}\n\nfunction render(batt, budget, budgetLive) {\n batt = Math.round(batt);\n budget = Math.round(budget);\n var margin = batt - budget;\n var barPct = batt > 0 ? Math.min(100, Math.max(0, (margin / batt) * 100)) : 0;\n var battSev = batt > 40 ? 'green' : batt > 20 ? 'amber' : 'red';\n var sev, msg, barColor;\n if (margin < CRIT) {\n sev = 'red'; msg = 'CRITICAL - return now';\n barColor = '#ff3a5a';\n } else if (margin < WARN) {\n sev = 'amber'; msg = 'Low headroom - consider returning';\n barColor = '#ffb830';\n } else {\n sev = 'green'; msg = 'Sufficient headroom';\n barColor = '#00e09a';\n }\n battValEl.textContent = batt + '%';\n budgetValEl.textContent = budget + '%';\n headroomValEl.textContent= margin + '%';\n setColour(battValEl, battSev);\n setColour(headroomValEl, sev);\n setColour(statusTextEl, sev);\n barFillEl.style.width = barPct + '%';\n barFillEl.style.background = barColor;\n statusTextEl.textContent = msg;\n footerEl.textContent = budgetLive\n ? 'Updated ' + new Date().toLocaleTimeString()\n : 'Updated ' + new Date().toLocaleTimeString() + ' (budget pending)';\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined') { statusTextEl.textContent = 'API not ready'; return; }\n var battRaw = window.cockpit.getDataLakeValue(VAR_BATTERY);\n if (battRaw === null || battRaw === undefined) { statusTextEl.textContent = 'Waiting for battery data'; return; }\n var batt = parseInt(battRaw, 10);\n if (batt === -1) { statusTextEl.textContent = 'Battery % not reported by vehicle'; battValEl.textContent = '??%'; return; }\n var budgetRaw = window.cockpit.getDataLakeValue(VAR_BUDGET);\n var budgetLive = budgetRaw !== null && budgetRaw !== undefined;\n render(batt, budgetLive ? parseFloat(budgetRaw) : 0, budgetLive);\n } catch(err) { statusTextEl.textContent = 'Error'; console.error('[Budget]', err); }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);" + "js": "// W5 -- Return Budget\n// Reads battery % from SYS_STATUS/battery_remaining (native ArduSub).\n// Reads return budget from external/rov-return-budget (cockpit_bridge, future).\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// Headroom = battery% - required%. AMBER<15% RED<5%\nvar W5_VAR_BATTERY = 'SYS_STATUS/battery_remaining';\nvar W5_VAR_BUDGET = 'external/rov-return-budget';\nvar W5_WARN = 15;\nvar W5_CRIT = 5;\nvar W5_POLL_MS = 1000;\n\nfunction w5SetColour(el, sev) {\n el.classList.remove('green', 'amber', 'red');\n if (sev) { el.classList.add(sev); }\n}\n\nfunction w5Render(batt, budget, budgetLive) {\n var battValEl = document.getElementById('w5-batt-val');\n var budgetValEl = document.getElementById('w5-budget-val');\n var headroomValEl = document.getElementById('w5-headroom-val');\n var barFillEl = document.getElementById('w5-bar-fill');\n var statusTextEl = document.getElementById('w5-status-text');\n var footerEl = document.getElementById('w5-footer');\n batt = Math.round(batt);\n budget = Math.round(budget);\n var margin = batt - budget;\n var barPct = batt > 0 ? Math.min(100, Math.max(0, (margin / batt) * 100)) : 0;\n var battSev = batt > 40 ? 'green' : batt > 20 ? 'amber' : 'red';\n var sev, msg, barColor;\n if (margin < W5_CRIT) {\n sev = 'red'; msg = 'CRITICAL - return now'; barColor = '#ff3a5a';\n } else if (margin < W5_WARN) {\n sev = 'amber'; msg = 'Low headroom - consider returning'; barColor = '#ffb830';\n } else {\n sev = 'green'; msg = 'Sufficient headroom'; barColor = '#00e09a';\n }\n battValEl.textContent = batt + '%';\n budgetValEl.textContent = budget + '%';\n headroomValEl.textContent = margin + '%';\n w5SetColour(battValEl, battSev);\n w5SetColour(headroomValEl, sev);\n w5SetColour(statusTextEl, sev);\n barFillEl.style.width = barPct + '%';\n barFillEl.style.background = barColor;\n statusTextEl.textContent = msg;\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString() +\n (budgetLive ? '' : ' (budget pending)');\n}\n\nfunction w5Poll() {\n var statusTextEl = document.getElementById('w5-status-text');\n try {\n if (typeof window.cockpit === 'undefined') { statusTextEl.textContent = 'API not ready'; return; }\n var battRaw = window.cockpit.getDataLakeVariableData(W5_VAR_BATTERY);\n if (battRaw === null || battRaw === undefined) { statusTextEl.textContent = 'Waiting for battery data'; return; }\n var batt = parseInt(battRaw, 10);\n if (batt === -1) {\n statusTextEl.textContent = 'Battery % not reported by vehicle';\n document.getElementById('w5-batt-val').textContent = '??%';\n return;\n }\n var budgetRaw = window.cockpit.getDataLakeVariableData(W5_VAR_BUDGET);\n var budgetLive = budgetRaw !== null && budgetRaw !== undefined;\n w5Render(batt, budgetLive ? parseFloat(budgetRaw) : 0, budgetLive);\n } catch(err) { statusTextEl.textContent = 'Error: ' + err.message; }\n}\n\nsetTimeout(w5Poll, 300);\nsetInterval(w5Poll, W5_POLL_MS);" }