fix(w1): move cause text to footer, keep message line state-driven to stop flicker

This commit is contained in:
Grant 2026-07-11 17:31:31 +02:00
parent 3e923609b8
commit 3409ae5739

View File

@ -1,7 +1,7 @@
{
"html": "<div id=\"w1-circles\">\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-green\" class=\"w1-circle\"></div>\n <span id=\"w1-l-green\" class=\"w1-circle-label\">Green</span>\n </div>\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-amber\" class=\"w1-circle\"></div>\n <span id=\"w1-l-amber\" class=\"w1-circle-label\">Amber</span>\n </div>\n <div class=\"w1-circle-wrap\">\n <div id=\"w1-c-red\" class=\"w1-circle\"></div>\n <span id=\"w1-l-red\" class=\"w1-circle-label\">Red</span>\n </div>\n</div>\n<div id=\"w1-message\">Connecting...</div>\n<div id=\"w1-footer\">--</div>",
"css": ".w1-circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; } .w1-circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; } .w1-circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; } .w1-circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; } .w1-circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; } .w1-circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; } .w1-circle-label.active { color: #d8eeff; font-weight: bold; } #w1-circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; } #w1-message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; } #w1-message.green { color: #00e09a; } #w1-message.amber { color: #ffb830; } #w1-message.red { color: #ff3a5a; } #w1-footer { margin-top: 6px; font-size: 9px; color: #2a4060; font-family: monospace; }",
"css": ".w1-circle-wrap { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 6px; } .w1-circle { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #1e3050; background: #1a2d42; } .w1-circle.green { background: #00e09a; border-color: #00e09a; box-shadow: 0 0 12px #00e09a; } .w1-circle.amber { background: #ffb830; border-color: #ffb830; box-shadow: 0 0 12px #ffb830; } .w1-circle.red { background: #ff3a5a; border-color: #ff3a5a; box-shadow: 0 0 16px #ff3a5a; } .w1-circle-label { font-size: 9px; color: #6a9bbf; text-transform: uppercase; letter-spacing: 0.08em; font-family: monospace; } .w1-circle-label.active { color: #d8eeff; font-weight: bold; } #w1-circles { display: flex; align-items: center; justify-content: center; margin-bottom: 10px; } #w1-message { font-size: 11px; color: #6a9bbf; text-align: center; max-width: 180px; line-height: 1.4; font-family: monospace; } #w1-message.green { color: #00e09a; } #w1-message.amber { color: #ffb830; } #w1-message.red { color: #ff3a5a; } #w1-footer { margin-top: 6px; font-size: 11px; color: #ff3a5a; font-family: monospace; }",
"js": "// W1 -- System Health Indicator\n// Reads external/rov-failsafe from Cockpit data lake.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=GREEN 1=AMBER 2=RED null=waiting\n//\n// Two-variable split:\n// W1_VAR (external/rov-failsafe) -- integer 0/1/2, drives the\n// circle colour + label.\n// W1_CAUSE_VAR (external/rov-failsafe-cause) -- string, e.g.\n// \"Heartbeat Lost\", empty\n// when nominal. Drives the\n// #w1-message text for\n// AMBER/RED only -- GREEN\n// and the waiting state\n// ignore it and always show\n// fixed text.\nvar W1_VAR = 'external/rov-failsafe';\nvar W1_CAUSE_VAR = 'external/rov-failsafe-cause';\nvar W1_POLL_MS = 500;\n\nfunction w1ClearAll() {\n document.getElementById('w1-c-green').className = 'w1-circle';\n document.getElementById('w1-c-amber').className = 'w1-circle';\n document.getElementById('w1-c-red').className = 'w1-circle';\n document.getElementById('w1-l-green').className = 'w1-circle-label';\n document.getElementById('w1-l-amber').className = 'w1-circle-label';\n document.getElementById('w1-l-red').className = 'w1-circle-label';\n document.getElementById('w1-message').className = '';\n}\n\n// state: 0=GREEN 1=AMBER 2=RED -1=waiting (see w1Poll)\n// cause: string from W1_CAUSE_VAR, '' when there is no cause text\n// (nominal, or the cause variable has not published yet).\nfunction w1ApplyState(state, cause) {\n w1ClearAll();\n var msgEl = document.getElementById('w1-message');\n if (state === 0) {\n // GREEN -- always fixed text; cause is ignored even if non-empty.\n document.getElementById('w1-c-green').className = 'w1-circle green';\n document.getElementById('w1-l-green').className = 'w1-circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n // AMBER -- prefer the cause text (e.g. \"Heartbeat Lost\") when present,\n // fall back to the generic label if the cause variable is empty.\n document.getElementById('w1-c-amber').className = 'w1-circle amber';\n document.getElementById('w1-l-amber').className = 'w1-circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = cause ? cause : 'Parameter degraded';\n } else if (state === 2) {\n // RED -- same cause-text-first behaviour as AMBER.\n document.getElementById('w1-c-red').className = 'w1-circle red';\n document.getElementById('w1-l-red').className = 'w1-circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = cause ? cause : 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction w1Poll() {\n var footEl = document.getElementById('w1-footer');\n try {\n if (typeof window.cockpit === 'undefined') {\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W1_VAR);\n if (raw === null || raw === undefined) {\n // No colour data yet -- call with a consistent (-1, '') signature\n // rather than omitting the second argument.\n w1ApplyState(-1, '');\n footEl.textContent = 'Waiting for ' + W1_VAR;\n return;\n }\n // Cause text is read independently of the colour state above, every\n // poll (not just on AMBER/RED), so the fallback below always has a\n // definite '' rather than undefined if the cause variable has not\n // published yet.\n var causeRaw = window.cockpit.getDataLakeVariableData(W1_CAUSE_VAR);\n var cause = (causeRaw === null || causeRaw === undefined) ? '' : causeRaw;\n w1ApplyState(parseInt(raw, 10), cause);\n // Footer stays the widget's own poll-health line -- 'OK' on a\n // successful poll, independent of the failsafe cause text above.\n footEl.textContent = 'OK';\n } catch(err) {\n footEl.textContent = 'Error: ' + err.message;\n }\n}\n\nsetTimeout(w1Poll, 300);\nsetInterval(w1Poll, W1_POLL_MS);"
"js": "// W1 -- System Health Indicator\n// Reads external/rov-failsafe from Cockpit data lake.\n// Getter confirmed: window.cockpit.getDataLakeVariableData(id)\n// State: 0=GREEN 1=AMBER 2=RED null=waiting\n//\n// Two-variable split, message-line vs footer:\n// W1_VAR (external/rov-failsafe) -- integer 0/1/2, drives the\n// circle colour + label AND\n// the #w1-message text\n// (fixed words only).\n// W1_CAUSE_VAR (external/rov-failsafe-cause) -- string, e.g.\n// \"Heartbeat Lost\", empty\n// when nominal. Drives\n// #w1-footer only.\n//\n// Why two separate lines instead of one combined line: rov-failsafe and\n// rov-failsafe-cause are independent data lake variables that update on\n// slightly different polls of cockpit_bridge. This widget previously put\n// the cause text into #w1-message itself (falling back to a generic word\n// when cause was empty), which caused a visible flicker between the cause\n// and the generic word as the two variables raced each other on every\n// poll. Keeping #w1-message driven ONLY by the state integer (fixed words,\n// so it never flickers) and moving the cause text to #w1-footer (plain\n// text, not colour/word-coded, so momentary staleness there is not\n// misleading) fixes the flicker while still surfacing the cause somewhere\n// on the widget.\nvar W1_VAR = 'external/rov-failsafe';\nvar W1_CAUSE_VAR = 'external/rov-failsafe-cause';\nvar W1_POLL_MS = 500;\n\nfunction w1ClearAll() {\n document.getElementById('w1-c-green').className = 'w1-circle';\n document.getElementById('w1-c-amber').className = 'w1-circle';\n document.getElementById('w1-c-red').className = 'w1-circle';\n document.getElementById('w1-l-green').className = 'w1-circle-label';\n document.getElementById('w1-l-amber').className = 'w1-circle-label';\n document.getElementById('w1-l-red').className = 'w1-circle-label';\n document.getElementById('w1-message').className = '';\n}\n\n// state: 0=GREEN 1=AMBER 2=RED -1=waiting (see w1Poll)\n// Sets #w1-message to fixed words ONLY -- no cause text here (see the\n// message-line-vs-footer split comment above). This is what stops the\n// flicker: this line only ever changes when the state integer itself\n// changes, never on a cause-text update alone.\nfunction w1ApplyState(state) {\n w1ClearAll();\n var msgEl = document.getElementById('w1-message');\n if (state === 0) {\n document.getElementById('w1-c-green').className = 'w1-circle green';\n document.getElementById('w1-l-green').className = 'w1-circle-label active';\n msgEl.className = 'green';\n msgEl.textContent = 'Systems nominal';\n } else if (state === 1) {\n document.getElementById('w1-c-amber').className = 'w1-circle amber';\n document.getElementById('w1-l-amber').className = 'w1-circle-label active';\n msgEl.className = 'amber';\n msgEl.textContent = 'Parameter degraded';\n } else if (state === 2) {\n document.getElementById('w1-c-red').className = 'w1-circle red';\n document.getElementById('w1-l-red').className = 'w1-circle-label active';\n msgEl.className = 'red';\n msgEl.textContent = 'CRITICAL';\n } else {\n msgEl.textContent = 'Waiting for data...';\n }\n}\n\nfunction w1Poll() {\n var footEl = document.getElementById('w1-footer');\n try {\n if (typeof window.cockpit === 'undefined') {\n // Poll/health problem -- the footer's diagnostic role takes priority\n // over showing a cause.\n footEl.textContent = 'API not ready';\n return;\n }\n var raw = window.cockpit.getDataLakeVariableData(W1_VAR);\n if (raw === null || raw === undefined) {\n // Poll/health problem -- same priority as above.\n w1ApplyState(-1);\n footEl.textContent = 'Waiting for ' + W1_VAR;\n return;\n }\n w1ApplyState(parseInt(raw, 10));\n // Footer now shows the failsafe CAUSE text, not a poll-health 'OK'.\n // Read independently of the state integer above -- a stale/lagging\n // cause here is a plain-text label, not a colour or generic word, so\n // it cannot visually flicker the way #w1-message used to.\n var causeRaw = window.cockpit.getDataLakeVariableData(W1_CAUSE_VAR);\n var cause = (causeRaw === null || causeRaw === undefined) ? '' : causeRaw;\n footEl.textContent = cause ? cause : '';\n } catch(err) {\n // Poll/health problem -- same priority as above.\n footEl.textContent = 'Error: ' + err.message;\n }\n}\n\nsetTimeout(w1Poll, 300);\nsetInterval(w1Poll, W1_POLL_MS);"
}