M1b: mobile app-bar clock — Design's resting clock + STALE reaction (Hybrid)
Design's mobile app bar shows a wall clock, no SYNC/STALE readout. Building it literally would drop the Ship-A staleness signal on the device most users are on. Kev's ruling: Hybrid — the wall clock is the RESTING state (the stillness), STALE/amber is the REACTION (the punctuation), driven by the SAME real signal as desktop (refreshed_at vs expected_interval_s, thresholds 1.5×/3×). Never silently stale on mobile. Design drew the happy path; we keep the failure state. - web/src/lib/freshness.js — extracted the freshness tier as ONE shared source (CommonJS, unit-tested); desktop HeartbeatBar + mobile clock both key off it, so mobile can't silently disagree with desktop about staleness. - LiveLayer: <768px hides SIGNAL LIVE + EKG + graded + the SYNC label and shows a single right-aligned clock — ticking wall clock when calm, amber SYNC / red STALE when the tier reacts. Resting dot is static (the clock is the pulse). - Test-lock: freshness.test.js (tier thresholds, absent≠false-stale, no negative age) + vyndrParityQA (mobile clock wired, one shared freshness source). Built to Design's mobile spec, VISUALLY UNVERIFIED at 390px. NEXT shell increments (still M1b): merge the clock into the logo row (Design's single-row app bar), the breadth strip (EDGES/AVG CLV/GAMES — replaces the graded count mobile lost here), and relocate the wire to the bottom of the board. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared pipeline-freshness tier (Ship A + design-train M1b).
|
||||
*
|
||||
* Measures the INTRADAY HEARTBEAT (`refreshed_at`, ~every 20 min) against the
|
||||
* pipeline's expected cadence — NOT the grade-lock time (`updated_at`, 5×/day,
|
||||
* intentionally stable). Measuring the wrong field is what produced the
|
||||
* "SIGNAL LIVE vs STALE 8h" contradiction (Ship A).
|
||||
*
|
||||
* ONE source for desktop (HeartbeatBar) AND mobile (the app-bar clock): the
|
||||
* mobile clock rests as a wall clock and only reacts — amber, then red STALE —
|
||||
* when this tier crosses. "Alive is punctuated stillness": the clock is the
|
||||
* stillness, STALE is the punctuation. Design drew the happy path; we never
|
||||
* silently go stale on the device most users are on.
|
||||
*
|
||||
* Thresholds (same both surfaces): calm < 1.5× · amber ≥ 1.5× · red ≥ 3×.
|
||||
*
|
||||
* CommonJS so it's unit-testable by the plain-JS Jest suite AND importable by
|
||||
* the .tsx client components (allowJs).
|
||||
*/
|
||||
|
||||
const DEFAULT_EXPECTED_S = 18000; // 5h max cron gap fallback (matches LiveLayer)
|
||||
|
||||
/**
|
||||
* @param {number} refreshedAtMs Date.parse(refreshed_at); NaN/absent → calm.
|
||||
* @param {number} expectedIntervalS summary.expected_interval_s (s); ≤0 → default.
|
||||
* @param {number} nowMs Date.now().
|
||||
* @returns {{ tier: 'normal'|'amber'|'stale', elapsedMs: number|null }}
|
||||
*/
|
||||
function freshnessTier(refreshedAtMs, expectedIntervalS, nowMs) {
|
||||
if (!Number.isFinite(refreshedAtMs)) return { tier: 'normal', elapsedMs: null };
|
||||
const expectedMs = (Number(expectedIntervalS) > 0 ? Number(expectedIntervalS) : DEFAULT_EXPECTED_S) * 1000;
|
||||
const elapsedMs = Math.max(0, nowMs - refreshedAtMs);
|
||||
const tier = elapsedMs >= 3 * expectedMs ? 'stale' : elapsedMs >= 1.5 * expectedMs ? 'amber' : 'normal';
|
||||
return { tier, elapsedMs };
|
||||
}
|
||||
|
||||
module.exports = { freshnessTier, DEFAULT_EXPECTED_S };
|
||||
Reference in New Issue
Block a user