From 53ebfc8dc6501f91f90e3ab640d6e95efac49279 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 12 May 2026 00:40:40 +0200 Subject: [PATCH] fix: rebuild all widgets as JSON, remove CSS globals and unicode --- widgets/README.md | 59 ++++++++++++------- widgets/files.zip | Bin 0 -> 12489 bytes widgets/w0_data_lake_inspector.json | 7 +++ widgets/w1_system_health_indicator copy.json | 7 +++ widgets/w1_system_health_indicator.json | 7 +++ widgets/w2_mission_status copy.json | 7 +++ widgets/w2_mission_status.json | 7 +++ widgets/w3_abort_button copy.json | 7 +++ widgets/w3_abort_button.json | 7 +++ widgets/w4_mission_setup_button copy.json | 7 +++ widgets/w4_mission_setup_button.json | 7 +++ widgets/w5_battery_return_budget copy.json | 7 +++ widgets/w5_battery_return_budget.json | 7 +++ 13 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 widgets/files.zip create mode 100644 widgets/w0_data_lake_inspector.json create mode 100644 widgets/w1_system_health_indicator copy.json create mode 100644 widgets/w1_system_health_indicator.json create mode 100644 widgets/w2_mission_status copy.json create mode 100644 widgets/w2_mission_status.json create mode 100644 widgets/w3_abort_button copy.json create mode 100644 widgets/w3_abort_button.json create mode 100644 widgets/w4_mission_setup_button copy.json create mode 100644 widgets/w4_mission_setup_button.json create mode 100644 widgets/w5_battery_return_budget copy.json create mode 100644 widgets/w5_battery_return_budget.json diff --git a/widgets/README.md b/widgets/README.md index 07c29f9..128a815 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -1,38 +1,57 @@ # Argonaut 3 — Cockpit DIY Widgets +## 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 + ## How to install a widget -1. Open Cockpit in your browser -2. Enter edit mode (pencil icon, top right) -3. Click **Add Widget** at the bottom of the screen -4. Scroll right to find the `` DIY widget — drag it onto the view -5. Click the **gear icon** on the widget to open the editor -6. Click **Import** and select the `.html` file for the widget you want -7. The widget will load immediately +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 + +> **Note:** Widget files are `.json` format (not `.html`). +> Cockpit expects a JSON file with `html`, `css`, and `js` fields. ## Widget index | File | Widget | Notes | |---|---|---| -| w0_data_lake_inspector.html | Data Lake Inspector | Diagnostic — shows all data lake variables. Not production. | -| w1_system_health_indicator.html | System Health Indicator | Traffic light GREEN/AMBER/RED. Reads rov_failsafe from data lake. | -| w2_mission_status.html | Mission Status | Progress bar + state. Reads rov_mission_state and rov_mission_progress. | -| w3_abort_button.html | Abort Button | Confirm dialog → POST /abort to FastAPI. Edit FASTAPI_HOST before use. | -| w4_mission_setup_button.html | Mission Setup Button | Opens setup page in new tab. Edit SETUP_URL before use. | -| w5_battery_return_budget.html | Return Budget | Headroom warning. Reads SYS_STATUS/battery_remaining (native) + rov_return_budget. | +| 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`. | ## Config values to edit before field use -In `w3_abort_button.html`: - +In `w3_abort_button.json` (line 1 of the JS section): ``` -const FASTAPI_HOST = 'http://blueos.local:8081'; +var FASTAPI_HOST = 'http://blueos.local:8081'; ``` -In `w4_mission_setup_button.html`: - +In `w4_mission_setup_button.json` (line 1 of the JS section): ``` -const SETUP_URL = 'http://blueos.local:8081/setup'; +var SETUP_URL = 'http://blueos.local:8081/setup'; ``` -Both values are correct for real hardware. For dev VM, use the VM's Tailscale IP instead of `blueos.local`. +Both values are correct for real hardware. For the dev VM, replace `blueos.local` with the VM Tailscale IP. + +## Widget data dependencies + +| 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 | diff --git a/widgets/files.zip b/widgets/files.zip new file mode 100644 index 0000000000000000000000000000000000000000..575c7b5c29536f604969969d923319574385f6cd GIT binary patch literal 12489 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 V`kPb%05E@#qrdNRobg|0{|itWQ+oga literal 0 HcmV?d00001 diff --git a/widgets/w0_data_lake_inspector.json b/widgets/w0_data_lake_inspector.json new file mode 100644 index 0000000..4920cb1 --- /dev/null +++ b/widgets/w0_data_lake_inspector.json @@ -0,0 +1,7 @@ +{ + "html": "
\n ⬡ Data Lake\n \n 0 vars\n connecting\u2026\n
\n
\n \n \n \n \n \n \n \n \n \n
VariableTypeValue
\n
No variables match the filter.
\n
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg0: #09111a;\n --bg1: #0e1d2e;\n --bg2: #162336;\n --border: #1e3050;\n --text0: #d8eeff;\n --text1: #6a9bbf;\n --accent: #00c8f0;\n --green: #00e09a;\n --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg0);\n color: var(--text0);\n font-family: var(--mono);\n font-size: 12px;\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n#header {\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n padding: 5px 8px;\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n#header .title { font-size: 11px; font-weight: bold; color: var(--accent); white-space: nowrap; }\n#search {\n flex: 1;\n background: var(--bg0);\n border: 1px solid var(--border);\n border-radius: 3px;\n color: var(--text0);\n font-family: var(--mono);\n font-size: 11px;\n padding: 2px 6px;\n outline: none;\n}\n#search:focus { border-color: var(--accent); }\n#count { font-size: 10px; color: var(--text1); white-space: nowrap; }\n#status { font-size: 10px; color: #ffb830; white-space: nowrap; }\n#status.ok { color: var(--green); }\n#status.err { color: #ff3a5a; }\n#table-wrap { flex: 1; overflow-y: auto; overflow-x: hidden; }\n#table-wrap::-webkit-scrollbar { width: 5px; }\n#table-wrap::-webkit-scrollbar-track { background: var(--bg0); }\n#table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\ntable { width: 100%; border-collapse: collapse; }\nthead th {\n position: sticky; top: 0;\n background: var(--bg2);\n border-bottom: 1px solid var(--border);\n color: var(--text1);\n font-size: 10px; font-weight: normal;\n padding: 3px 6px; text-align: left;\n text-transform: uppercase; letter-spacing: 0.05em;\n z-index: 1;\n}\nthead th:nth-child(1) { width: 55%; }\nthead th:nth-child(2) { width: 12%; }\nthead th:nth-child(3) { width: 33%; }\ntbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }\ntbody tr:hover { background: var(--bg2); }\ntbody tr.flash td { color: var(--green); }\ntd { padding: 3px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\ntd:nth-child(1) { color: var(--accent); font-size: 11px; }\ntd:nth-child(2) { color: var(--text1); font-size: 10px; }\ntd:nth-child(3) { color: var(--text0); font-size: 11px; }\n#empty { padding: 20px; text-align: center; color: var(--text1); font-size: 11px; }", + + "js": "// State\nlet vars = {};\nlet filter = '';\n\n// DOM\nconst tbody = document.getElementById('tbody');\nconst countEl = document.getElementById('count');\nconst statusEl= document.getElementById('status');\nconst searchEl= document.getElementById('search');\nconst emptyEl = document.getElementById('empty');\n\n// Format a value for display\nfunction fmt(v) {\n if (v === null || v === undefined) return '\u2014';\n if (typeof v === 'number') return Number.isInteger(v) ? String(v) : v.toFixed(4);\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (typeof v === 'object') return JSON.stringify(v).slice(0, 60);\n return String(v).slice(0, 80);\n}\n\n// Render table\nfunction render() {\n const keys = Object.keys(vars).sort();\n const filtered = keys.filter(k => !filter || k.toLowerCase().includes(filter));\n emptyEl.style.display = filtered.length === 0 ? '' : 'none';\n countEl.textContent = filtered.length + ' / ' + keys.length + ' vars';\n const existingRows = {};\n tbody.querySelectorAll('tr[data-key]').forEach(row => { existingRows[row.dataset.key] = row; });\n Object.keys(existingRows).forEach(k => { if (!filtered.includes(k)) { existingRows[k].remove(); delete existingRows[k]; } });\n filtered.forEach(function(key) {\n const d = vars[key];\n let row = existingRows[key];\n if (!row) {\n row = document.createElement('tr');\n row.dataset.key = key;\n row.innerHTML = '' + key + '';\n tbody.appendChild(row);\n }\n const cells = row.cells;\n const newType = d.type || '\u2014';\n const newValue = fmt(d.value);\n if (cells[2].textContent !== newValue) {\n row.classList.add('flash');\n setTimeout(function() { row.classList.remove('flash'); }, 400);\n }\n cells[1].textContent = newType;\n cells[2].textContent = newValue;\n });\n}\n\n// Poll data lake\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined' || typeof window.cockpit.getAllDataLakeVariablesInfo !== 'function') {\n statusEl.className = 'err';\n statusEl.textContent = 'API not ready';\n return;\n }\n const info = window.cockpit.getAllDataLakeVariablesInfo();\n if (!info || Object.keys(info).length === 0) {\n statusEl.className = '';\n statusEl.textContent = 'no data';\n return;\n }\n Object.entries(info).forEach(function(entry) {\n const key = entry[0]; const meta = entry[1];\n vars[key] = { type: meta && meta.type ? meta.type : (typeof (meta && meta.value)), value: meta && meta.value !== undefined ? meta.value : meta };\n });\n statusEl.className = 'ok';\n statusEl.textContent = 'live';\n render();\n } catch(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'error';\n console.error('[DataLake]', err);\n }\n}\n\n// Filter handler\nsearchEl.addEventListener('input', function() { filter = searchEl.value.trim().toLowerCase(); render(); });\n\n// Start\nsetTimeout(poll, 300);\nsetInterval(poll, 500);" +} diff --git a/widgets/w1_system_health_indicator copy.json b/widgets/w1_system_health_indicator copy.json new file mode 100644 index 0000000..d455650 --- /dev/null +++ b/widgets/w1_system_health_indicator copy.json @@ -0,0 +1,7 @@ +{ + "html": "
\n
\n
\n Green\n
\n
\n
\n Amber\n
\n
\n
\n Red\n
\n
\n
Connecting\u2026
\n
\u2014
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #09111a;\n --text0: #d8eeff;\n --text1: #6a9bbf;\n --green: #00e09a;\n --amber: #ffb830;\n --red: #ff3a5a;\n --dim: #1a2d42;\n --border: #1e3050;\n --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg);\n color: var(--text0);\n font-family: var(--mono);\n height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n user-select: none;\n}\n#circles { display: flex; gap: 12px; align-items: center; justify-content: center; margin-bottom: 10px; }\n.circle-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }\n.circle {\n width: 36px; height: 36px;\n border-radius: 50%;\n border: 2px solid var(--border);\n background: var(--dim);\n transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;\n}\n.circle.green { background: var(--green); border-color: var(--green); box-shadow: 0 0 12px var(--green); }\n.circle.amber { background: var(--amber); border-color: var(--amber); box-shadow: 0 0 12px var(--amber); }\n.circle.red { background: var(--red); border-color: var(--red); animation: flash-red 1s ease-in-out infinite; }\n@keyframes flash-red {\n 0%, 100% { box-shadow: 0 0 16px var(--red); }\n 50% { box-shadow: 0 0 32px var(--red), 0 0 8px #ff000088; }\n}\n.circle-label { font-size: 9px; color: var(--text1); text-transform: uppercase; letter-spacing: 0.08em; }\n.circle-label.active { color: var(--text0); font-weight: bold; }\n#message { font-size: 11px; color: var(--text1); text-align: center; max-width: 180px; line-height: 1.4; min-height: 16px; }\n#message.green { color: var(--green); }\n#message.amber { color: var(--amber); }\n#message.red { color: var(--red); }\n#footer { margin-top: 6px; font-size: 9px; color: #2a4060; }", + + "js": "// Config\nvar VARIABLE = 'rov_failsafe';\nvar POLL_MS = 500;\nvar STATE_GREEN = 0, STATE_AMBER = 1, STATE_RED = 2;\nvar MESSAGES = {};\nMESSAGES[STATE_GREEN] = 'Systems nominal';\nMESSAGES[STATE_AMBER] = 'Parameter degraded \u2014 check system';\nMESSAGES[STATE_RED] = 'CRITICAL \u2014 Safe action triggered';\n\n// DOM\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 applyState(state, reason) {\n [cGreen, cAmber, cRed].forEach(function(c) { c.classList.remove('green','amber','red'); });\n [lGreen, lAmber, lRed].forEach(function(l) { l.classList.remove('active'); });\n msgEl.classList.remove('green','amber','red');\n if (state === STATE_GREEN) {\n cGreen.classList.add('green'); lGreen.classList.add('active');\n msgEl.classList.add('green'); msgEl.textContent = reason || MESSAGES[STATE_GREEN];\n } else if (state === STATE_AMBER) {\n cAmber.classList.add('amber'); lAmber.classList.add('active');\n msgEl.classList.add('amber'); msgEl.textContent = reason || MESSAGES[STATE_AMBER];\n } else if (state === STATE_RED) {\n cRed.classList.add('red'); lRed.classList.add('active');\n msgEl.classList.add('red'); msgEl.textContent = reason || MESSAGES[STATE_RED];\n } else {\n msgEl.textContent = 'Waiting for data\u2026';\n }\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined' || typeof window.cockpit.getDataLakeValue !== 'function') {\n applyState(null); footEl.textContent = 'API not ready'; return;\n }\n var raw = window.cockpit.getDataLakeValue(VARIABLE);\n if (raw === null || raw === undefined) {\n applyState(null); footEl.textContent = 'Waiting for ' + VARIABLE; return;\n }\n applyState(parseInt(raw, 10), null);\n footEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n } catch(err) {\n applyState(null); footEl.textContent = 'Poll error';\n console.error('[Health Widget]', err);\n }\n}\n\nsetTimeout(poll, 300);\nsetInterval(poll, POLL_MS);" +} diff --git a/widgets/w1_system_health_indicator.json b/widgets/w1_system_health_indicator.json new file mode 100644 index 0000000..48bb3b9 --- /dev/null +++ b/widgets/w1_system_health_indicator.json @@ -0,0 +1,7 @@ +{ + "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; }", + + "js": "var VARIABLE = 'rov_failsafe';\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 diff --git a/widgets/w2_mission_status copy.json b/widgets/w2_mission_status copy.json new file mode 100644 index 0000000..be11e1c --- /dev/null +++ b/widgets/w2_mission_status copy.json @@ -0,0 +1,7 @@ +{ + "html": "
\u2014
\n
\n
\u2014%
\n
Waypoint: \u2014
\n
Waiting for rov_mission_state\u2026
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #09111a;\n --border: #1e3050;\n --text0: #d8eeff;\n --text1: #6a9bbf;\n --green: #00e09a;\n --amber: #ffb830;\n --red: #ff3a5a;\n --blue: #00c8f0;\n --dim: #1a2d42;\n --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg); color: var(--text0); font-family: var(--mono);\n height: 100vh; display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n overflow: hidden; padding: 10px 14px; gap: 8px;\n}\n#state-label {\n font-size: 18px; font-weight: bold;\n letter-spacing: 0.12em; text-transform: uppercase; color: var(--text1);\n}\n#state-label.idle { color: var(--text1); }\n#state-label.running { color: var(--green); }\n#state-label.paused { color: var(--amber); }\n#state-label.complete { color: var(--blue); }\n#state-label.aborted { color: var(--red); }\n#state-label.waiting { color: var(--dim); }\n#bar-wrap {\n width: 100%; height: 8px;\n background: var(--dim); border-radius: 4px;\n border: 1px solid var(--border); overflow: hidden;\n}\n#bar-fill { height: 100%; width: 0%; border-radius: 4px; background: var(--green); transition: width 0.5s ease, background 0.3s; }\n#pct-label { font-size: 11px; color: var(--text1); align-self: flex-end; margin-top: -4px; }\n#waypoint { font-size: 10px; color: var(--text1); text-align: center; }\n#footer { font-size: 9px; color: #1e3050; }", + + "js": "var VAR_STATE = 'rov_mission_state';\nvar VAR_PROGRESS = 'rov_mission_progress';\nvar POLL_MS = 500;\nvar STATE = {0:'IDLE',1:'RUNNING',2:'PAUSED',3:'COMPLETE',4:'ABORTED'};\nvar META = {\n 0:{cls:'idle', bar:'#1a2d42'},\n 1:{cls:'running', bar:'#00e09a'},\n 2:{cls:'paused', bar:'#ffb830'},\n 3:{cls:'complete',bar:'#00c8f0'},\n 4:{cls:'aborted', bar:'#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 stateStr = STATE[state] || '?';\n var meta = META[state] || {cls:'waiting', bar:'#1a2d42'};\n var pct = Math.round((progress || 0) * 100);\n stateLabelEl.className = meta.cls;\n stateLabelEl.textContent = stateStr;\n barFillEl.style.width = pct + '%';\n barFillEl.style.background = meta.bar;\n pctLabelEl.textContent = (state===1||state===2) ? pct+'%' : (state===3 ? '100%' : '\u2014%');\n if (state===1||state===2) waypointEl.textContent = 'Progress ' + pct + '% complete';\n else if (state===3) waypointEl.textContent = 'Mission complete';\n else if (state===4) waypointEl.textContent = 'Mission aborted \u2014 vehicle holding';\n else waypointEl.textContent = 'No mission active';\n footerEl.textContent = 'Updated ' + new Date().toLocaleTimeString();\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined' || typeof window.cockpit.getDataLakeValue !== 'function') {\n stateLabelEl.className = 'waiting'; stateLabelEl.textContent = '\u2014';\n footerEl.textContent = 'API not ready'; return;\n }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) {\n stateLabelEl.className = 'waiting'; stateLabelEl.textContent = '\u2014';\n footerEl.textContent = 'Waiting for ' + VAR_STATE; return;\n }\n var prog = window.cockpit.getDataLakeValue(VAR_PROGRESS);\n render(parseInt(raw,10), parseFloat(prog||0));\n } catch(err) {\n stateLabelEl.className='waiting'; stateLabelEl.textContent='ERR';\n footerEl.textContent='Poll error'; console.error('[MissionStatus]',err);\n }\n}\nsetTimeout(poll,300); setInterval(poll,POLL_MS);" +} diff --git a/widgets/w2_mission_status.json b/widgets/w2_mission_status.json new file mode 100644 index 0000000..8757002 --- /dev/null +++ b/widgets/w2_mission_status.json @@ -0,0 +1,7 @@ +{ + "html": "
--
\n
\n
--%
\n
No mission active
\n
Waiting for rov_mission_state
", + + "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}", + + "js": "var VAR_STATE = 'rov_mission_state';\nvar VAR_PROGRESS = 'rov_mission_progress';\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);" +} diff --git a/widgets/w3_abort_button copy.json b/widgets/w3_abort_button copy.json new file mode 100644 index 0000000..d65395c --- /dev/null +++ b/widgets/w3_abort_button copy.json @@ -0,0 +1,7 @@ +{ + "html": "\n
Standby
\n
\n
\n

Abort mission?
Vehicle will hold position
and await instruction.

\n
\n \n \n
\n
\n
\n
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #09111a; --text0: #d8eeff; --text1: #8fb3d4;\n --red: #ff3a5a; --red-dim: #7a1a28; --green: #00e09a; --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg); color: var(--text0); font-family: var(--mono);\n height: 100vh; display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n overflow: hidden; padding: 12px; gap: 10px;\n}\n#abort-btn {\n width: 100%; padding: 14px 0;\n background: var(--red-dim); border: 2px solid var(--red); border-radius: 6px;\n color: var(--red); font-family: var(--mono); font-size: 15px; font-weight: bold;\n letter-spacing: 0.15em; text-transform: uppercase; cursor: pointer;\n transition: background 0.15s, box-shadow 0.15s;\n}\n#abort-btn:hover { background: #2a0c18; box-shadow: 0 0 16px var(--red); }\n#abort-btn:active { background: var(--red); color: #fff; }\n#abort-btn:disabled { background: #1a0a10; border-color: #3a1020; color: var(--red-dim); cursor: not-allowed; box-shadow: none; }\n#confirm-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(0,0,0,0.7);\n align-items: center; justify-content: center; z-index: 10;\n}\n#confirm-overlay.active { display: flex; }\n#confirm-box {\n background: #100510; border: 2px solid var(--red); border-radius: 8px;\n padding: 18px; max-width: 220px; text-align: center;\n box-shadow: 0 0 30px var(--red);\n}\n#confirm-box p { font-size: 12px; color: var(--text0); line-height: 1.5; margin-bottom: 14px; }\n.btn-row { display: flex; gap: 8px; justify-content: center; }\n#confirm-yes {\n flex: 1; padding: 8px; background: var(--red); border: none;\n border-radius: 4px; color: #fff; font-family: var(--mono);\n font-size: 12px; font-weight: bold; cursor: pointer; letter-spacing: 0.08em;\n}\n#confirm-yes:hover { background: #ff6080; }\n#confirm-no {\n flex: 1; padding: 8px; background: transparent;\n border: 1px solid var(--text1); border-radius: 4px;\n color: var(--text1); font-family: var(--mono); font-size: 12px; cursor: pointer;\n}\n#confirm-no:hover { border-color: var(--text0); color: var(--text0); }\n#countdown { font-size: 10px; color: var(--red-dim); margin-top: 8px; }\n#status { font-size: 10px; color: var(--text1); text-align: center; min-height: 14px; }\n#status.ok { color: var(--green); }\n#status.err { color: var(--red); }", + + "js": "var FASTAPI_HOST = 'http://blueos.local:8081';\nvar AUTO_DISMISS_SEC = 10;\nvar abortBtn = document.getElementById('abort-btn');\nvar overlay = document.getElementById('confirm-overlay');\nvar confirmYes = document.getElementById('confirm-yes');\nvar confirmNo = document.getElementById('confirm-no');\nvar countdownEl = document.getElementById('countdown');\nvar statusEl = document.getElementById('status');\nvar dismissTimer = null;\nvar countdownVal = AUTO_DISMISS_SEC;\n\nfunction showConfirm() {\n overlay.classList.add('active');\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 overlay.classList.remove('active');\n clearInterval(dismissTimer);\n dismissTimer = null;\n countdownEl.textContent = '';\n}\n\nasync function sendAbort() {\n hideConfirm();\n abortBtn.disabled = true;\n statusEl.className = '';\n statusEl.textContent = 'Sending abort\u2026';\n try {\n var res = await fetch(FASTAPI_HOST + '/abort', {\n method: 'POST',\n headers: {'Content-Type':'application/json'},\n body: JSON.stringify({abort:true}),\n signal: AbortSignal.timeout(5000)\n });\n if (res.ok) {\n statusEl.className = 'ok';\n statusEl.textContent = 'Abort sent \u2014 vehicle holding';\n } else {\n statusEl.className = 'err';\n statusEl.textContent = 'Server error: ' + res.status;\n abortBtn.disabled = false;\n }\n } catch(err) {\n statusEl.className = 'err';\n statusEl.textContent = 'Network error \u2014 check connection';\n abortBtn.disabled = false;\n console.error('[Abort]', err);\n }\n}\n\nabortBtn.addEventListener('click', function() { if (!abortBtn.disabled) showConfirm(); });\nconfirmYes.addEventListener('click', sendAbort);\nconfirmNo.addEventListener('click', hideConfirm);" +} diff --git a/widgets/w3_abort_button.json b/widgets/w3_abort_button.json new file mode 100644 index 0000000..6e756e5 --- /dev/null +++ b/widgets/w3_abort_button.json @@ -0,0 +1,7 @@ +{ + "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; }", + + "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);" +} diff --git a/widgets/w4_mission_setup_button copy.json b/widgets/w4_mission_setup_button copy.json new file mode 100644 index 0000000..1ba13e8 --- /dev/null +++ b/widgets/w4_mission_setup_button copy.json @@ -0,0 +1,7 @@ +{ + "html": "\n
\u2014
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #09111a; --border: #1e3050;\n --text0: #d8eeff; --text1: #6a9bbf;\n --accent: #00c8f0; --dim: #1a2d42; --amber: #ffb830;\n --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg); color: var(--text0); font-family: var(--mono);\n height: 100vh; display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n overflow: hidden; padding: 10px; gap: 6px;\n}\n#setup-btn {\n width: 100%; padding: 10px 0;\n background: transparent; border: 1px solid var(--accent); border-radius: 5px;\n color: var(--accent); font-family: var(--mono); font-size: 12px; font-weight: bold;\n letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer;\n transition: background 0.2s, border-color 0.2s, color 0.2s, opacity 0.3s;\n}\n#setup-btn:hover { background: rgba(0,200,240,0.1); box-shadow: 0 0 10px rgba(0,200,240,0.3); }\n#setup-btn.subdued { border-color: var(--dim); color: var(--text1); opacity: 0.5; }\n#setup-btn.subdued:hover { opacity: 0.75; background: transparent; box-shadow: none; }\n#status { font-size: 9px; color: var(--text1); text-align: center; }\n#status.active-warn { color: var(--amber); }", + + "js": "var SETUP_URL = 'http://blueos.local:8081/setup';\nvar VAR_STATE = 'rov_mission_state';\nvar POLL_MS = 1000;\nvar setupBtn = document.getElementById('setup-btn');\nvar statusEl = document.getElementById('status');\n\nsetupBtn.addEventListener('click', function() {\n window.open(SETUP_URL, '_blank', 'noopener,noreferrer');\n});\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined' || typeof window.cockpit.getDataLakeValue !== 'function') {\n setupBtn.classList.remove('subdued'); statusEl.className = '';\n statusEl.textContent = 'Connecting\u2026'; return;\n }\n var raw = window.cockpit.getDataLakeValue(VAR_STATE);\n if (raw === null || raw === undefined) {\n setupBtn.classList.remove('subdued'); statusEl.className = '';\n statusEl.textContent = 'No mission loaded'; return;\n }\n var state = parseInt(raw, 10);\n if (state === 1 || state === 2) {\n setupBtn.classList.add('subdued'); statusEl.className = 'active-warn';\n statusEl.textContent = state === 1\n ? 'Mission running \u2014 setup changes not recommended'\n : 'Mission paused \u2014 setup changes not recommended';\n } else {\n setupBtn.classList.remove('subdued'); statusEl.className = '';\n statusEl.textContent = state === 0\n ? 'No mission loaded \u2014 configure before diving'\n : 'Ready to configure next mission';\n }\n } catch(err) { console.error('[SetupBtn]', err); }\n}\nsetTimeout(poll, 300); setInterval(poll, POLL_MS);" +} diff --git a/widgets/w4_mission_setup_button.json b/widgets/w4_mission_setup_button.json new file mode 100644 index 0000000..b748202 --- /dev/null +++ b/widgets/w4_mission_setup_button.json @@ -0,0 +1,7 @@ +{ + "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; }", + + "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);" +} diff --git a/widgets/w5_battery_return_budget copy.json b/widgets/w5_battery_return_budget copy.json new file mode 100644 index 0000000..673c968 --- /dev/null +++ b/widgets/w5_battery_return_budget copy.json @@ -0,0 +1,7 @@ +{ + "html": "Return Budget\n
\n Battery\n --%\n
\n
\n Required\n --%\n
\n
\n
\n
\n Headroom\n --\n
\n
\n
\n
Waiting for data\u2026
\n
\u2014
", + + "css": "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #09111a; --border: #1e3050;\n --text0: #d8eeff; --text1: #6a9bbf;\n --green: #00e09a; --amber: #ffb830; --red: #ff3a5a; --dim: #1a2d42;\n --mono: 'Courier New', monospace;\n}\nbody {\n background: var(--bg); color: var(--text0); font-family: var(--mono);\n height: 100vh; display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n overflow: hidden; padding: 8px 12px; gap: 5px;\n}\n.section-label { font-size: 9px; color: var(--text1); text-transform: uppercase; letter-spacing: 0.1em; align-self: flex-start; }\n.data-row { display: flex; width: 100%; justify-content: space-between; align-items: baseline; gap: 8px; }\n.data-label { font-size: 10px; color: var(--text1); white-space: nowrap; }\n.data-value { font-size: 16px; font-weight: bold; color: var(--text0); text-align: right; min-width: 48px; transition: color 0.3s; }\n.divider { width: 100%; height: 1px; background: var(--border); flex-shrink: 0; }\n#headroom-wrap { width: 100%; }\n#headroom-bar-bg { width: 100%; height: 5px; background: var(--dim); border-radius: 3px; overflow: hidden; margin-top: 2px; }\n#headroom-bar-fill { height: 100%; border-radius: 3px; background: var(--green); transition: width 0.5s ease, background 0.3s; }\n#status-text { font-size: 10px; color: var(--text1); text-align: center; min-height: 12px; transition: color 0.3s; }\n.green { color: var(--green) !important; }\n.amber { color: var(--amber) !important; }\n.red { color: var(--red) !important; }\n#footer { font-size: 9px; color: #1a2d42; margin-top: 1px; }", + + "js": "var VAR_BATTERY = 'SYS_STATUS/battery_remaining';\nvar VAR_BUDGET = 'rov_return_budget';\nvar WARN = 15, CRIT = 5, POLL_MS = 1000;\nvar battValEl = document.getElementById('batt-val');\nvar budgetValEl = document.getElementById('budget-val');\nvar headroomValEl = document.getElementById('headroom-val');\nvar headroomFillEl = document.getElementById('headroom-bar-fill');\nvar statusTextEl = document.getElementById('status-text');\nvar footerEl = document.getElementById('footer');\n\nfunction setClass(el, cls) { el.classList.remove('green','amber','red'); if(cls) el.classList.add(cls); }\n\nfunction render(batt, budget, budgetLive) {\n batt = Math.round(batt);\n budget = Math.round(budget);\n var margin = batt - budget;\n battValEl.textContent = batt + '%';\n setClass(battValEl, batt > 40 ? 'green' : batt > 20 ? 'amber' : 'red');\n budgetValEl.textContent = budget + '%';\n headroomValEl.textContent = margin + '%';\n var barPct = batt > 0 ? Math.min(100, Math.max(0, (margin / batt) * 100)) : 0;\n headroomFillEl.style.width = barPct + '%';\n var sev, msg;\n if (margin < CRIT) { sev = 'red'; msg = '\u26a0 CRITICAL \u2014 insufficient return budget'; }\n else if (margin < WARN) { sev = 'amber'; msg = 'Low headroom \u2014 consider returning'; }\n else { sev = 'green'; msg = 'Sufficient return headroom'; }\n var colors = {green:'var(--green)', amber:'var(--amber)', red:'var(--red)'};\n headroomFillEl.style.background = colors[sev];\n setClass(headroomValEl, sev); setClass(statusTextEl, sev);\n statusTextEl.textContent = msg;\n footerEl.textContent = budgetLive\n ? 'Updated ' + new Date().toLocaleTimeString()\n : 'Updated ' + new Date().toLocaleTimeString() + ' (budget: FastAPI pending)';\n}\n\nfunction poll() {\n try {\n if (typeof window.cockpit === 'undefined' || typeof window.cockpit.getDataLakeValue !== 'function') {\n statusTextEl.textContent = 'API not ready'; return;\n }\n var battRaw = window.cockpit.getDataLakeValue(VAR_BATTERY);\n if (battRaw === null || battRaw === undefined) { statusTextEl.textContent = 'Waiting for battery data\u2026'; 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='Poll error'; console.error('[Budget]',err); }\n}\nsetTimeout(poll,300); setInterval(poll,POLL_MS);" +} diff --git a/widgets/w5_battery_return_budget.json b/widgets/w5_battery_return_budget.json new file mode 100644 index 0000000..da4c570 --- /dev/null +++ b/widgets/w5_battery_return_budget.json @@ -0,0 +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
--
", + + "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}", + + "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);" +}