Overflow Tap: the tile says what Tap is on; the corner dot says how it went
The "TAP IS ON · since …" line (and its SWITCHING / NOT SWITCHED forms, and
the red error line under the destination) are gone from the tile. A tile
shows the keyword Tap is on, its destination, the groups moved, the slide
chip and the trail; status is the corner dot's job, as everywhere else in
ProdDash.
The dot now follows the retries honestly: a switch Overflow refuses is
retried three times, ten seconds apart, with the dot amber ("retrying (n of
4 attempts failed)") while the module is still fixing it itself; only when
the attempts are spent does the dot go red with the reason on hover, and the
alert fire if it is switched on. The budget counts refusals in a row across
keywords, not per keyword — a service keeps bringing new keywords, and each
one restarting a fresh budget meant a dead Overflow never turned the dot
red. The alert is raised once per outage, and a success clears the streak.
Verified against the mock with --fail-switch: amber through three refusals,
red on the fourth with one alert, later refusals stay red without
re-alerting, and green again on the first success once the mock is back.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
227e3bb846
commit
6fffefc6c0
@@ -11,6 +11,11 @@
|
||||
* tap a disc follows what is on screen. A slide with no keyword changes
|
||||
* nothing; the last destination stays until the next keyworded slide.
|
||||
*
|
||||
* A switch Overflow refuses is retried three times, ten seconds apart, with
|
||||
* the tile's dot left alone meanwhile; only when those are spent does the
|
||||
* dot go red (and the alert fire, if switched on) — the booth hears about a
|
||||
* problem the module could not fix itself, not about every hiccup.
|
||||
*
|
||||
* GET /state → the view-model every tile draws
|
||||
* GET /stream → SSE, `state` events on every change (heartbeated)
|
||||
* POST /refresh → re-read the keyword mappings from the Dashboard now
|
||||
@@ -124,6 +129,14 @@ module.exports = {
|
||||
let retriesLeft = 0;
|
||||
let retryTimer = null;
|
||||
let alertRaised = false;
|
||||
/**
|
||||
* Refusals in a row, across keywords. The retry budget is per keyword, but
|
||||
* a service keeps bringing new keywords: if Overflow refuses every one of
|
||||
* them, each would start a fresh budget and the dot would never go red.
|
||||
* So the budget is also spent by refusals as a whole, and a success
|
||||
* clears it.
|
||||
*/
|
||||
let consecutiveFailures = 0;
|
||||
|
||||
// Presentation layer: the slide /v1/status/slide reports.
|
||||
let lastSlideKey = null;
|
||||
@@ -171,10 +184,15 @@ module.exports = {
|
||||
if (!of.reachable) return ['error', of.error || 'Signing in to Overflow…'];
|
||||
if (!pp.reachable) return ['error', pp.error ? `ProPresenter unreachable: ${pp.error}` : 'Reaching ProPresenter…'];
|
||||
if (!state.keywords.length) return ['error', 'No keyword mappings in the Overflow Dashboard (Integrations › ProPresenter)'];
|
||||
if (state.current && !state.current.pending && !state.current.ok) {
|
||||
return ['error', `Tap not switched to ${state.current.keyword} — ${state.current.error}`];
|
||||
// A refused switch the module is still retrying on its own is amber —
|
||||
// being worked on; one it has given up on is red — the booth's problem.
|
||||
const cur = state.current;
|
||||
if (cur && !cur.ok && !cur.pending && !cur.retrying) {
|
||||
return ['error', `Tap not switched to ${cur.keyword} — ${cur.error}`];
|
||||
}
|
||||
if (cur && !cur.ok && (cur.retrying || consecutiveFailures > 0)) {
|
||||
return ['connecting', `Tap not switched to ${cur.keyword} yet — retrying (${consecutiveFailures} of ${SWITCH_RETRIES + 1} attempts failed): ${cur.error || 'in flight'}`];
|
||||
}
|
||||
if (state.current && state.current.pending) return ['connecting', `Switching Tap to ${state.current.keyword}…`];
|
||||
const n = state.keywords.length;
|
||||
const who = forAdmin && session && session.email ? `${session.email}, ` : '';
|
||||
return ['ok', `Following ${host}:${port} · Overflow: ${who}${n} keyword${n === 1 ? '' : 's'}`];
|
||||
@@ -381,6 +399,7 @@ module.exports = {
|
||||
at: Date.now(),
|
||||
ok: false,
|
||||
pending: true,
|
||||
retrying: false, // a retry is scheduled: the module is still trying to fix this itself
|
||||
error: '',
|
||||
attempts: 0,
|
||||
};
|
||||
@@ -399,22 +418,33 @@ module.exports = {
|
||||
}
|
||||
entry.ok = true;
|
||||
entry.error = '';
|
||||
entry.retrying = false;
|
||||
entry.at = Date.now();
|
||||
consecutiveFailures = 0;
|
||||
clearAlert();
|
||||
log(`Tap → ${entry.destination || '?'} (${target.keyword})${entry.groups.length ? ` on ${entry.groups.join(', ')}` : ''}`);
|
||||
} catch (err) {
|
||||
if (stopped) return;
|
||||
entry.ok = false;
|
||||
entry.error = describe(err);
|
||||
log(`Tap not switched to ${target.keyword}: ${entry.error}`);
|
||||
if (retriesLeft > 0) {
|
||||
consecutiveFailures += 1;
|
||||
if (retriesLeft > 0 && consecutiveFailures <= SWITCH_RETRIES) {
|
||||
// Still the module's problem: try again shortly; the dot goes amber.
|
||||
retriesLeft -= 1;
|
||||
entry.retrying = true;
|
||||
log(`Tap not switched to ${target.keyword}: ${entry.error} — retrying in ${SWITCH_RETRY_MS / 1000} s`);
|
||||
retryTimer = later(() => {
|
||||
retryTimer = null;
|
||||
runSwitch();
|
||||
}, SWITCH_RETRY_MS);
|
||||
} else {
|
||||
// Out of retries: now it is the booth's problem — red dot, and the
|
||||
// alert if the operator asked for one (raised once per outage, not
|
||||
// again for every further keyword Overflow refuses).
|
||||
entry.retrying = false;
|
||||
log(`Tap not switched to ${target.keyword}: ${entry.error} — giving up after ${consecutiveFailures} refusals`);
|
||||
if (!alertRaised) raiseAlert(target.keyword);
|
||||
}
|
||||
raiseAlert(target.keyword);
|
||||
} finally {
|
||||
entry.pending = false;
|
||||
inFlight = false;
|
||||
|
||||
Reference in New Issue
Block a user