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 0000000..575c7b5
Binary files /dev/null and b/widgets/files.zip differ
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\n
\n \n \n Variable \n Type \n Value \n \n \n \n
\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": "\nConnecting\u2026
\n",
+
+ "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": "\nConnecting...
\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%
\nWaypoint: \u2014
\n",
+
+ "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--%
\nNo mission active
\n",
+
+ "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": "\u26a0 ABORT MISSION \nStandby
\n\n
\n
Abort mission? Vehicle will hold position and await instruction.
\n
\n ABORT \n Cancel \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": "ABORT MISSION \nStandby
\n\n
Abort mission? Vehicle will hold position.
\n
\n CONFIRM ABORT \n Cancel \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": "\u2699 Mission Setup \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": "MISSION SETUP \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
\nWaiting for data\u2026
\n",
+
+ "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\nWaiting 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);"
+}