fix(control-pane): replace stale live-region guidance when a poll fails

The live region announced the advisory and steering state on every
change, but a failed or invalid poll only set the visible status to
offline. The live region kept the last known message, so during an
outage a screen-reader user could still hear "No steering is required"
from a poll that had already failed. That is worse than silence,
because stale guidance reads as current.

Route announcements through a single helper and have the failure path
replace the guidance with a message saying the data is unavailable and
the counts are unknown. The helper still suppresses a repeat of the
same message, so a continuing outage stays quiet and recovery speaks
again.

Cover the success then failure and failure then recovery sequences at
runtime. Reverting just the failure-path announcement fails the new
assertion, and the unmutated view passes all of them.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
pasmud
2026-09-27 02:01:44 +00:00
co-authored by CommandCodeBot
parent 6744a01748
commit 9e0996c6d6
2 changed files with 54 additions and 13 deletions
@@ -83,8 +83,8 @@ function renderControlPlaneViewHtml() {
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var view = { tasks: [], lanes: [], pairs: [], events: [], projection: { agents: [] }, thresholds: { ta: 0.35, ra: 0.7 } };
// Last advisory/steering pair handed to the live region, so a poll that
// changes nothing stays silent.
// Last message handed to the live region, so a poll that changes nothing
// stays silent.
var lastSpoken = null;
function resize() {
@@ -240,6 +240,15 @@ function renderControlPlaneViewHtml() {
});
}
// Polling runs every few seconds, so only speak when the advisory and
// steering counts actually move. Repeating an unchanged summary would talk
// over the operator without telling them anything new.
function announce(message) {
if (message === lastSpoken) return;
lastSpoken = message;
document.getElementById('announce').textContent = message;
}
function apply(data) {
if (!data || data.schemaVersion !== 'ecc.control-plane.view.v1' ||
!['tasks', 'lanes', 'pairs', 'events'].every(function (key) { return Array.isArray(data[key]); }) ||
@@ -259,17 +268,9 @@ function renderControlPlaneViewHtml() {
(c.advisories || 0) + ' advisories (' + (c.resolutions || 0) + ' steering)' +
(view.inventory && view.inventory.status !== 'ok' ? ' | inventory ' + view.inventory.status : '');
// Polling runs every few seconds, so only speak when the advisory and
// steering counts actually move. Repeating an unchanged summary would talk
// over the operator without telling them anything new.
var speaker = document.getElementById('announce');
var spoken = (c.advisories || 0) + '/' + (c.resolutions || 0);
if (spoken !== lastSpoken) {
lastSpoken = spoken;
speaker.textContent = (c.advisories || 0) + ' advisories, ' +
(c.resolutions || 0) + ' steering. ' +
((c.resolutions || 0) > 0 ? 'Steering is required.' : 'No steering is required.');
}
announce((c.advisories || 0) + ' advisories, ' +
(c.resolutions || 0) + ' steering. ' +
((c.resolutions || 0) > 0 ? 'Steering is required.' : 'No steering is required.'));
}
function poll() {
@@ -278,6 +279,9 @@ function renderControlPlaneViewHtml() {
return r.json();
}).then(apply).catch(function () {
document.getElementById('status').textContent = 'offline';
// The last guidance is now stale, so replace it rather than leaving the
// live region claiming the airspace is clear.
announce('Control-plane data is unavailable. Advisories and steering are unknown.');
});
}
+37
View File
@@ -206,6 +206,43 @@ let passed = 0;
`a changed poll must announce the new counts, got ${changed.elements.get('announce').textContent}`);
passed += 1;
// An outage must not leave the live region holding the last known guidance,
// which would read as a current "airspace is clear" after data stopped.
const outage = await render([
{ ok: true, data: populatedView() },
{ ok: false, data: { ok: false, error: 'snapshot unavailable' } }
]);
const beforeOutage = outage.elements.get('announce').textContent;
await outage.pollAgain();
assert.strictEqual(outage.elements.get('status').textContent, 'offline');
assert.ok(outage.elements.get('announce').textContent !== beforeOutage,
'a failed poll must not leave the previous guidance in the live region');
assert.ok(/unavailable/i.test(outage.elements.get('announce').textContent)
&& /unknown/i.test(outage.elements.get('announce').textContent),
`an outage must say the counts are unknown, got ${outage.elements.get('announce').textContent}`);
passed += 1;
// A repeated failure stays silent, but recovering online must speak again.
const writesAfterOutage = outage.writesTo('announce');
await outage.pollAgain();
assert.strictEqual(outage.writesTo('announce'), writesAfterOutage,
'a repeated failure must not re-announce the same outage');
passed += 1;
const recovered = await render([
{ ok: true, data: populatedView() },
{ ok: false, data: { ok: false, error: 'snapshot unavailable' } },
{ ok: true, data: populatedView({ counts: { tasks: 3, lanes: 1, agents: 2, advisories: 1, resolutions: 1 } }) }
]);
await recovered.pollAgain();
await recovered.pollAgain();
assert.ok(recovered.elements.get('status').textContent.includes('3 tasks'),
'a recovered poll must restore the live status');
assert.ok(recovered.elements.get('announce').textContent.includes('1 advisories')
&& recovered.elements.get('announce').textContent.includes('Steering is required.'),
`a recovered poll must announce the restored counts, got ${recovered.elements.get('announce').textContent}`);
passed += 1;
console.log(`Results: Passed: ${passed}, Failed: 0`);
})().catch(error => {
console.error(error.message);