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:
Kev
2026-07-16 11:26:00 -04:00
parent c4d4617fab
commit 7c7ab24be5
5 changed files with 145 additions and 24 deletions
+39
View File
@@ -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 };