diff --git a/widgets/diagnostics/w_probe_api.json b/widgets/diagnostics/w_probe_api.json
deleted file mode 100644
index 9a18cda..0000000
--- a/widgets/diagnostics/w_probe_api.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "html": "
heading: waiting...
status: starting
",
-
- "css": "#probe-root { font-family: monospace; font-size: 11px; color: #00ff00; background: #111; padding: 6px; overflow-y: auto; height: 100%; } #probe-methods { margin-bottom: 6px; border-bottom: 1px solid #333; padding-bottom: 6px; } #probe-heading { font-size: 14px; color: #ffff00; margin-bottom: 4px; } #probe-status { color: #aaa; font-size: 10px; }",
-
- "js": "// ============================================================\n// Argonaut 3 -- window.cockpit API Probe Widget\n// Purpose: enumerate every method on window.cockpit, then\n// try every plausible data-lake read method for\n// 'external/rov-heading'. Run this once to confirm\n// the correct method name before building W1-W5.\n// Version: 1.0 Date: 2026-06-22\n// Source researched: github.com/bluerobotics/cockpit\n// src/libs/external-api/api.ts (commit 52fb1a2d)\n// The External API uses postMessage (iframes only).\n// DIY widgets run inline -- window.cockpit surface is\n// different and must be probed at runtime.\n// ============================================================\n\n// --- 1. ENUMERATE window.cockpit METHODS ---\n// Run immediately so the method list appears even if\n// data-lake calls fail.\n(function enumerateCockpitMethods() {\n var el = document.getElementById('probe-methods');\n var status = document.getElementById('probe-status');\n if (!el) return;\n\n // Check whether window.cockpit exists at all\n if (typeof window.cockpit === 'undefined') {\n el.innerText = 'ERROR: window.cockpit is undefined';\n if (status) status.innerText = 'status: cockpit object missing -- Pirate Mode enabled?';\n return;\n }\n\n // Collect all own + inherited enumerable keys\n var keys = [];\n for (var k in window.cockpit) {\n try {\n keys.push(k + ' [' + typeof window.cockpit[k] + ']');\n } catch (e) {\n keys.push(k + ' [access error]');\n }\n }\n\n // Also check Object.keys for non-enumerable own props\n var ownKeys = Object.keys(window.cockpit);\n var methodLine = 'window.cockpit methods (' + keys.length + '):\\n' + keys.join('\\n');\n el.innerText = methodLine;\n if (status) status.innerText = 'status: enumerated ' + keys.length + ' keys';\n})();\n\n// --- 2. PROBE DATA-LAKE READ METHODS ---\n// We try each candidate method in sequence.\n// The first one that returns a non-undefined value for\n// 'external/rov-heading' is the correct method for W1-W5.\n\n(function probeDataLake() {\n var headingEl = document.getElementById('probe-heading');\n var statusEl = document.getElementById('probe-status');\n if (!headingEl) return;\n if (typeof window.cockpit === 'undefined') return; // already flagged above\n\n // Candidate method names to probe, in priority order.\n // Based on source research:\n // - getAllDataLakeVariablesInfo() = CONFIRMED metadata-only (id/name/type)\n // - getDataLakeVariableData() = internal Cockpit function (utils-data-lake.ts)\n // - getDataLakeVariableInfo() = paired with above in same file\n // - getDataLakeValue() = mentioned in older docs, not confirmed\n // - listenToDatalakeVariable() = External API (postMessage, for iframes)\n var VARIABLE_ID = 'external/rov-heading';\n var candidates = [\n 'getDataLakeVariableData',\n 'getDataLakeValue',\n 'getDataLakeVariableInfo',\n 'listenToDatalakeVariable'\n ];\n\n // Track which methods exist vs are callable\n var results = [];\n\n // Try synchronous getters first (candidates 0-2)\n var syncCandidates = candidates.slice(0, 3);\n for (var i = 0; i < syncCandidates.length; i++) {\n var name = syncCandidates[i];\n if (typeof window.cockpit[name] !== 'function') {\n results.push(name + ': NOT a function (type=' + typeof window.cockpit[name] + ')');\n continue;\n }\n try {\n var result = window.cockpit[name](VARIABLE_ID);\n if (result !== undefined && result !== null) {\n // Found it -- display the live value\n results.push(name + ': WORKS -> ' + JSON.stringify(result));\n headingEl.innerText = 'heading (' + name + '): ' + JSON.stringify(result);\n statusEl.innerText = 'status: live read confirmed via ' + name;\n } else {\n results.push(name + ': returned undefined/null');\n }\n } catch (e) {\n results.push(name + ': threw -> ' + e.message);\n }\n }\n\n // Try callback-based listenToDatalakeVariable last\n // It registers a callback, not a sync return\n var listenName = 'listenToDatalakeVariable';\n if (typeof window.cockpit[listenName] === 'function') {\n try {\n window.cockpit[listenName](VARIABLE_ID, function(value) {\n var el = document.getElementById('probe-heading');\n if (el) el.innerText = 'heading (listenToDatalakeVariable callback): ' + JSON.stringify(value);\n var sel = document.getElementById('probe-status');\n if (sel) sel.innerText = 'status: live callback from listenToDatalakeVariable';\n });\n results.push(listenName + ': registered callback (watching for value...)');\n } catch (e) {\n results.push(listenName + ': threw -> ' + e.message);\n }\n } else {\n results.push(listenName + ': NOT a function');\n }\n\n // Append probe results under the method list\n var methodsEl = document.getElementById('probe-methods');\n if (methodsEl) {\n methodsEl.innerText += '\\n\\n--- DATA LAKE PROBE ---\\nvar=' + VARIABLE_ID + '\\n' + results.join('\\n');\n }\n})();\n\n// --- 3. LIVE POLL --- \n// If any sync getter worked, re-call it every 2s so we can\n// confirm it tracks live updates (heading should change as\n// the cockpit_bridge publishes new values).\n// We do NOT know which method works yet, so we poll all\n// sync candidates that exist and update the display.\n(function livePoll() {\n var VARIABLE_ID = 'external/rov-heading';\n var syncCandidates = ['getDataLakeVariableData', 'getDataLakeValue', 'getDataLakeVariableInfo'];\n\n // setInterval is allowed in DIY widget JS\n setInterval(function() {\n if (typeof window.cockpit === 'undefined') return;\n for (var i = 0; i < syncCandidates.length; i++) {\n var name = syncCandidates[i];\n if (typeof window.cockpit[name] !== 'function') continue;\n try {\n var val = window.cockpit[name](VARIABLE_ID);\n if (val !== undefined && val !== null) {\n var el = document.getElementById('probe-heading');\n if (el) el.innerText = 'heading (' + name + '): ' + JSON.stringify(val);\n var sel = document.getElementById('probe-status');\n if (sel) sel.innerText = 'status: polling OK via ' + name;\n return; // first working method wins\n }\n } catch (e) { /* silent -- error shown in method list above */ }\n }\n }, 2000);\n})();"
-}
diff --git a/widgets/w6_altitude.json b/widgets/w6_altitude.json
new file mode 100644
index 0000000..bdd827d
--- /dev/null
+++ b/widgets/w6_altitude.json
@@ -0,0 +1,9 @@
+{
+ "html": "",
+
+ "css": "/* ============================================================\n W6 — Altitude Widget\n Displays downward Ping2 sonar altitude in metres.\n Colour bands: GREEN >1.5m / AMBER 0.5-1.5m / RED <0.5m\n Shows NO SONAR when no data is available (sentinel -1).\n Style: matches W1-W5 dark theme.\n ============================================================ */\n\n/* Root container — fills the Cockpit widget panel */\n#w6-root {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n height: 100%;\n padding: 10px 8px;\n box-sizing: border-box;\n background: #0d1117;\n font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;\n color: #c9d1d9;\n user-select: none;\n}\n\n/* Header block — title and subtitle */\n#w6-header {\n text-align: center;\n width: 100%;\n}\n\n/* Widget title */\n#w6-title {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: #8b949e;\n margin-bottom: 2px;\n}\n\n/* Widget subtitle — sensor identification */\n#w6-subtitle {\n font-size: 9px;\n color: #484f58;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n\n/* Body — bar gauge + numeric readout side by side */\n#w6-body {\n display: flex;\n flex-direction: row;\n align-items: flex-end;\n justify-content: center;\n gap: 12px;\n flex: 1;\n width: 100%;\n padding: 8px 0;\n}\n\n/* Vertical bar track — the background of the altitude bar */\n#w6-bar-track {\n position: relative;\n width: 18px;\n height: 90px;\n background: #161b22;\n border: 1px solid #30363d;\n border-radius: 3px;\n overflow: hidden;\n}\n\n/* Colour zone overlays — these are static backgrounds showing\n the threshold bands even when no data is present.\n Critical zone: bottom 10% of bar (0 - 0.5m)\n Caution zone: 10-30% of bar (0.5 - 1.5m)\n Good zone: 30-100% (above 1.5m) is the default dark track */\n#w6-bar-critical-zone {\n position: absolute;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 10%; /* Represents 0 - 0.5m on the 0-5m scale */\n background: rgba(239, 68, 68, 0.15); /* Faint red tint */\n border-top: 1px dashed rgba(239, 68, 68, 0.3);\n}\n\n#w6-bar-caution-zone {\n position: absolute;\n bottom: 10%;\n left: 0;\n width: 100%;\n height: 20%; /* Represents 0.5m - 1.5m on the 0-5m scale */\n background: rgba(245, 158, 11, 0.10); /* Faint amber tint */\n border-top: 1px dashed rgba(245, 158, 11, 0.3);\n}\n\n/* Animated fill bar — height driven by JS based on altitude value */\n#w6-bar-fill {\n position: absolute;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 0%; /* Updated by JS */\n background: #00e5a0; /* Default green; updated by JS based on state */\n border-radius: 2px 2px 0 0;\n transition: height 0.4s ease, background 0.3s ease; /* Smooth transitions */\n}\n\n/* Threshold labels on the bar — 1.5m and 0.5m markers */\n#w6-bar-label-caution,\n#w6-bar-label-critical {\n position: absolute;\n right: -22px; /* Float labels to the right of the bar */\n font-size: 7px;\n color: #484f58;\n font-family: monospace;\n white-space: nowrap;\n}\n\n#w6-bar-label-caution { bottom: calc(30% - 4px); } /* At 1.5m threshold */\n#w6-bar-label-critical { bottom: calc(10% - 4px); } /* At 0.5m threshold */\n\n/* Numeric readout block — large altitude value */\n#w6-readout {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: flex-end;\n}\n\n/* Large altitude number — colour updated by JS */\n#w6-value {\n font-size: 36px;\n font-weight: 700;\n font-variant-numeric: tabular-nums; /* Prevents layout shift as digits change */\n color: #8b949e; /* Default grey; updated by JS */\n line-height: 1;\n letter-spacing: -0.02em;\n transition: color 0.3s ease;\n}\n\n/* Unit label below the number */\n#w6-unit {\n font-size: 10px;\n color: #484f58;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-top: 3px;\n}\n\n/* Status label at the bottom — GOOD / CAUTION / CRITICAL / NO SONAR */\n#w6-status {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: #484f58; /* Default grey for NO SONAR; updated by JS */\n transition: color 0.3s ease;\n padding-top: 4px;\n}",
+
+ "js": "// ============================================================\n// W6 — Altitude Widget\n// Argonaut 3 — Cockpit DIY Widget\n//\n// Reads the downward Ping2 sonar altitude from the Cockpit\n// data lake via cockpit_bridge and displays it as a numeric\n// readout with a colour-coded vertical bar gauge.\n//\n// Data lake variable: external/rov-altitude\n// Source topic: /ping2/altitude (sensor_msgs/Range)\n// Published by: cockpit_bridge (ROS2 node on RPi5)\n//\n// Thresholds:\n// GOOD (GREEN) : altitude > 1.5m\n// CAUTION (AMBER) : 0.5m <= altitude <= 1.5m\n// CRITICAL (RED) : altitude < 0.5m\n// NO SONAR (GREY) : sentinel value -1.0 (no Ping2 data)\n//\n// Bar scale: linear, 0m (bottom) to 5m (top).\n// Altitudes above 5m fill the bar to 100%.\n//\n// Confirmed working Cockpit data lake read method:\n// window.cockpit.getDataLakeVariableData(variableId)\n// (confirmed via probe widget, documented UI Design v1.5)\n// ============================================================\n\n(function () {\n 'use strict';\n\n // ----------------------------------------------------------\n // Constants\n // ----------------------------------------------------------\n\n // Cockpit data lake variable ID populated by cockpit_bridge\n var DATA_LAKE_VAR = 'external/rov-altitude';\n\n // Altitude thresholds in metres\n var THRESHOLD_CRITICAL_M = 0.5; // Below this: RED (CRITICAL)\n var THRESHOLD_CAUTION_M = 1.5; // Below this: AMBER (CAUTION); above: GREEN (GOOD)\n\n // Bar gauge upper limit in metres.\n // Altitudes at or above this fill the bar to 100%.\n // Set to 5m — the operationally relevant near-structure range.\n var BAR_MAX_M = 5.0;\n\n // Sentinel value published by cockpit_bridge when no Ping2\n // data has been received (no sonar connected or no return).\n var SENTINEL_NO_DATA = -1.0;\n\n // Poll rate — 2Hz, matching cockpit_bridge broadcast rate\n var POLL_INTERVAL_MS = 500;\n\n // Colours — match W1-W5 project palette\n var COLOUR_GOOD = '#00e5a0'; // Green — safe altitude\n var COLOUR_CAUTION = '#f59e0b'; // Amber — low altitude warning\n var COLOUR_CRITICAL = '#ef4444'; // Red — critical altitude\n var COLOUR_NO_DATA = '#484f58'; // Grey — no sonar\n\n // ----------------------------------------------------------\n // DOM element references\n // Resolved once on load — the elements are guaranteed to\n // exist because this JS runs after the HTML is injected.\n // ----------------------------------------------------------\n var elValue = document.getElementById('w6-value');\n var elStatus = document.getElementById('w6-status');\n var elBarFill = document.getElementById('w6-bar-fill');\n\n // ----------------------------------------------------------\n // update()\n // Called at POLL_INTERVAL_MS. Reads the current altitude\n // from the data lake and updates the display accordingly.\n // ----------------------------------------------------------\n function update() {\n // Guard: Cockpit API must be present\n if (typeof window.cockpit === 'undefined' ||\n typeof window.cockpit.getDataLakeVariableData !== 'function') {\n setNoData('NO API');\n return;\n }\n\n // Read current altitude value from the data lake.\n // Returns a string representation of the float or null\n // if the variable has not yet been published.\n var raw = window.cockpit.getDataLakeVariableData(DATA_LAKE_VAR);\n\n // Parse to float — handles string '3.45', number 3.45, null, undefined\n var metres = parseFloat(raw);\n\n // ----------------------------------------------------------\n // Case 1: No data — sentinel value, NaN, or null\n // Shown when no Ping2 is connected or no echo returned.\n // ----------------------------------------------------------\n if (isNaN(metres) || metres <= SENTINEL_NO_DATA) {\n setNoData('NO SONAR');\n return;\n }\n\n // ----------------------------------------------------------\n // Case 2: Data received — determine status band and colour\n // ----------------------------------------------------------\n var colour, statusText;\n\n if (metres < THRESHOLD_CRITICAL_M) {\n // RED — vehicle is dangerously close to the structure/seafloor\n colour = COLOUR_CRITICAL;\n statusText = 'CRITICAL';\n } else if (metres < THRESHOLD_CAUTION_M) {\n // AMBER — altitude is low; operator awareness required\n colour = COLOUR_CAUTION;\n statusText = 'CAUTION';\n } else {\n // GREEN — altitude is safe for normal operations\n colour = COLOUR_GOOD;\n statusText = 'GOOD';\n }\n\n // ----------------------------------------------------------\n // Update DOM elements\n // ----------------------------------------------------------\n\n // Numeric readout — 2 decimal places, colour-coded\n elValue.textContent = metres.toFixed(2);\n elValue.style.color = colour;\n\n // Status label\n elStatus.textContent = statusText;\n elStatus.style.color = colour;\n\n // Bar fill height — linear scale from 0m to BAR_MAX_M\n // Clamped to [0, 100]% so values above BAR_MAX_M stay at 100%\n var fillPct = Math.min(Math.max(metres / BAR_MAX_M, 0), 1.0) * 100;\n elBarFill.style.height = fillPct.toFixed(1) + '%';\n elBarFill.style.background = colour;\n }\n\n // ----------------------------------------------------------\n // setNoData(label)\n // Resets all display elements to the no-data / no-sonar state.\n // Called when the sentinel value is read or the API is absent.\n // ----------------------------------------------------------\n function setNoData(label) {\n elValue.textContent = '--';\n elValue.style.color = COLOUR_NO_DATA;\n elStatus.textContent = label;\n elStatus.style.color = COLOUR_NO_DATA;\n elBarFill.style.height = '0%';\n elBarFill.style.background = COLOUR_NO_DATA;\n }\n\n // ----------------------------------------------------------\n // Startup\n // Run one immediate update so the widget does not show a\n // blank state for the first 500ms after load.\n // Then start the 2Hz poll loop.\n // ----------------------------------------------------------\n update();\n setInterval(update, POLL_INTERVAL_MS);\n\n}());",
+
+ "inheritCockpitStyles": false
+}