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
+46
View File
@@ -0,0 +1,46 @@
'use strict';
// Shared pipeline-freshness tier (Ship A + design-train M1b). The desktop
// HeartbeatBar and the mobile app-bar clock BOTH key off this — one source, so
// mobile can never silently disagree with desktop about staleness.
const { freshnessTier, DEFAULT_EXPECTED_S } = require('../../web/src/lib/freshness');
const S = 1000;
const now = 1_000_000_000_000;
describe('freshnessTier', () => {
test('calm when the heartbeat is recent (< 1.5× expected)', () => {
const r = freshnessTier(now - 1000 * S, 1200, now); // 1000s elapsed, expected 1200 → 0.83×
expect(r.tier).toBe('normal');
expect(r.elapsedMs).toBe(1000 * S);
});
test('amber at ≥ 1.5× expected (the reaction begins)', () => {
const r = freshnessTier(now - 1800 * S, 1200, now); // 1800/1200 = 1.5×
expect(r.tier).toBe('amber');
});
test('stale (red) at ≥ 3× expected — never silently stale', () => {
const r = freshnessTier(now - 3600 * S, 1200, now); // 3600/1200 = 3×
expect(r.tier).toBe('stale');
});
test('absent/NaN heartbeat is calm, not a false STALE', () => {
expect(freshnessTier(NaN, 1200, now).tier).toBe('normal');
expect(freshnessTier(NaN, 1200, now).elapsedMs).toBeNull();
});
test('non-positive expected falls back to the default 5h gap', () => {
// 4h elapsed vs the 5h (18000s) default → still under 1.5× → calm
const r = freshnessTier(now - 4 * 3600 * S, 0, now);
expect(DEFAULT_EXPECTED_S).toBe(18000);
expect(r.tier).toBe('normal');
});
test('future/negative elapsed clamps to 0 (calm), never a negative age', () => {
const r = freshnessTier(now + 5000 * S, 1200, now);
expect(r.elapsedMs).toBe(0);
expect(r.tier).toBe('normal');
});
});