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
+12 -6
View File
@@ -148,12 +148,18 @@ describe('QA.22 — honest waiting copy everywhere (no passive mystery)', () =>
describe('M1 — mobile foundation is wired (structural, not visual)', () => {
const css = read('app/globals.css');
it('header collapses on phones: the decorative EKG is hidden <768px', () => {
// The audit flagged ticker + SIGNAL LIVE + STALE stacking in ~110px.
expect(css).toMatch(/\.hb-ekg\s*\{[^}]*display:\s*none/);
// and the hook actually exists on the component
expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/hb-ekg/);
expect(read('components/vyndr/LiveLayer.tsx')).toMatch(/heartbeat-bar/);
it('mobile app-bar clock: desktop heartbeat pieces hidden, clock shown <768px', () => {
// Design's mobile drops SIGNAL LIVE + EKG + graded + SYNC-label for a single
// right-aligned clock (Hybrid: wall clock rests, STALE reacts).
expect(css).toMatch(/\.hb-ekg,\s*\.hb-desktop\s*\{[^}]*display:\s*none/);
expect(css).toMatch(/\.hb-mobile-clock\s*\{\s*display:\s*inline-flex/);
const live = read('components/vyndr/LiveLayer.tsx');
expect(live).toMatch(/hb-mobile-clock/);
expect(live).toMatch(/hb-desktop/);
// freshness is the ONE shared tier, not a re-implementation
expect(live).toMatch(/from '@\/lib\/freshness'/);
// never silently stale: the mobile clock reacts to the stale tier
expect(live).toMatch(/STALE/);
});
it('primary CTA meets a 44px touch target on mobile', () => {