Generated by /office-hours on 2026-05-19 Reviewed by /plan-eng-review on 2026-05-20 Reviewed by /plan-design-review on 2026-05-20 Branch: dghauri0/split-bar-tv-left-right Repo: ihnyc/avi-pub-landing Status: APPROVED (post-eng + post-design review) Mode: Builder

Problem Statement

The pub has two TVs mounted side-by-side on the same wall, both rendering /tv. The current page is a single layout split into two CSS columns (.tv-left = Featured Event + Slideshow; .tv-aside = Coming Up + Pub Hours + tender takeover). Because each TV mirrors the other, every panel is effectively rendered at half-of-a-TV width — too small to read from across the room. Bumping font sizes via CSS helped, but the panel still occupies ~50% of one screen’s width and hits a readability ceiling.

The two side-by-side TVs are functioning as one visual surface with 2x the pixels of a single screen, but the layout is using only 1x worth of real estate twice. The fix is to spread the layout across both displays so each panel can own a full TV.

What Makes This Cool

The pub wall stops looking like “two identical TVs” and starts looking like “the pub’s information surface.” Each TV gets a clear job: LEFT is what’s happening now (featured event, announcements, photos), RIGHT is what’s coming up + who’s playing right now. The “Now Playing” line that today is buried in the footer finally gets a hero card on the right TV when a song is active — the song-ID pipeline already exists, it just never had room to shine. Everything reuses the existing data layer, SSE, and rendering code. No new infrastructure.

Constraints

  • Two physical TVs (Tizen + Pi-driven) mounted side-by-side on the same wall, single visual surface.
  • Both TVs are visible to essentially every patron at once — content targeting is secondary to readability.
  • /tv must keep working unchanged until the new routes are wired into the TV remotes (no flag-day migration).
  • Must continue to support the special-event takeover system (SPECIAL_EVENT_TAKEOVER_PLAN.md) — takeovers span both TVs.
  • Bookmarkable URLs — when an operator needs to manually refresh a TV via the remote, the URL has to be typeable or already saved as a favorite. This kills query-param-based approaches.
  • Existing asset versioning uses ?v=... on script.js/styles.css — preserved as-is, no conflict.

Premises

  1. The fix is primarily layout, secondarily content targeting. Spread existing panels across more pixels, but allow each TV to surface content that earns a full-TV hero treatment (e.g., Now Playing on the right).
  2. Two separate route URLs (/bar-tv-left, /bar-tv-right) beat one URL with a ?screen= parameter. Each TV’s identity is its URL; testing on a laptop is trivial (visit either URL); the URL is self-documenting; manual refresh via the Tizen remote is dead-simple.
  3. The natural content split mirrors today’s columns, but is open to rebalancing. Left TV = “now” surface (Featured + Slideshow). Right TV = “planning + ambient” surface (Coming Up + Pub Hours + Now Playing hero).
  4. Takeovers span both TVs. A special-event takeover activates on both screens (body class drives layout swap on both).

Approaches Considered

Approach A — Mirror-split (minimal viable)

Two new routes serving thin HTML pages, each including only the panels for that side. Shared styles.css + script.js. Existing rendering logic reused 100%. No new content; just spreads what already exists. Effort: S. Risk: Low.

Approach B — Hero-split with content targeting (chosen)

Same routing as A, but each TV is redesigned as one hero panel + supporting rotation slot(s). LEFT TV gets a true Featured Event hero with massive typography and full-bleed photo treatment, plus the slideshow below at full width. RIGHT TV’s right-rail-stage rotation gains a Now Playing hero card (full-screen track + artist) that slots into the existing rotation when a song is active. Coming Up + Pub Hours sized up significantly. Effort: M. Risk: Med.

Approach C — Extended canvas (rejected)

Treat the two TVs as one super-wide canvas where the same conceptual layout spans both via CSS Grid + a display=L/R token. Panoramic photos can fill both. Cinematic. Effort: L. Risk: High. Sync drift, Tizen build-update reload mismatches, and takeover edge cases multiply the failure surface by 2. Not worth the sync tax for a pub display where each TV needs to fail gracefully if its sibling glitches.

Approach B, split into two phases. Phase 1 ships the layout split (the readability fix that motivated this work). Phase 2 adds the Now Playing hero card on the right TV — same scope as B’s content-targeting goal, but its own design + commits because it requires non-trivial changes to script.js rotation logic. Phase 1 is what gets the TVs readable from the back booth this week.

Reading-distance hierarchy

Implementer reference. All sizing and visual-weight decisions calibrate against this:

LEFT TV (1920x1080):
  PRIMARY (reads at 30 ft) → Featured Event title + time
  SECONDARY (reads at 15 ft) → Current slide content (announcement title or photo)
  TERTIARY (reads at 5 ft) → Slide indicator dots, sub-events line, footer items

RIGHT TV (1920x1080):
  PRIMARY (reads at 30 ft) → Next upcoming event (day + title)
  SECONDARY (reads at 15 ft) → Tonight's serving window (current day card highlight)
  TERTIARY (reads at 5 ft) → Remaining week events, remaining days' hours, footer

Typography scale per TV

Concrete vw-based scale locked during /plan-design-review. Implementer plugs these directly into CSS:

LEFT TV (body.bar-tv-left)
  Featured Event title       6vw    (PRIMARY)
  Featured time + badge      2.6vw  (PRIMARY)
  Photo caption              1.8vw  (SECONDARY)
  Slideshow title            4vw    (SECONDARY)
  Slideshow body             2vw    (SECONDARY)

RIGHT TV (body.bar-tv-right)
  Coming Up event title      3.2vw  (PRIMARY)
  Coming Up date             2.4vw  (PRIMARY)
  Coming Up time             2.2vw  (SECONDARY)
  Pub Hours day-label        2vw    (SECONDARY)
  Pub Hours time             2.4vw  (SECONDARY)

Both TVs
  Footer items               1.4vw  (TERTIARY)
  Section heading pills      ~1.6vw (TERTIARY, existing)

LEFT TV (/bar-tv-left) — “What’s Happening Now”

Layout (top to bottom):

  • Header (~8% of viewport): I-House Pub logo + date + clock (same as today).
  • Featured Event hero (~55% of viewport): the existing .panel--featured reflowed for full-TV scale. Title font ~5-7vw (up from ~3vw at half-width today). When a featured event has a photo, the photo becomes the dominant visual. When is-image-slide mode is active (image-only announcement), the photo goes truly full-bleed across both the hero zone AND the slideshow zone below — this is the “cinematic” moment.
  • Slideshow (~30% of viewport): .panel--announcements at full TV width. Each announcement slide gets ~2x today’s horizontal space. Slideshow dots indicator stays. Slide progress bar stays. Event mosaic (takeover) renders here.
  • Footer (~7% of viewport): tender + serving hours + refresh indicator + AT logo (same as today). The “Now Playing” footer line is suppressed on the left TV because the right TV will own that surface — avoids redundancy at scale.

RIGHT TV (/bar-tv-right) — “Coming Up + Ambient”

Phase 1 layout (top to bottom):

  • Header (~8% of viewport): same as left.
  • Right-rail-stage (~85% of viewport): the existing two-slide rotation (#right-rail-overview#right-rail-flyer), unchanged in behavior, panels sized up.
    • Overview slide (default): Coming Up list (top ~55%) + Pub Hours grid (bottom ~45%), both at much larger font.
    • Flyer slide (existing): unchanged behavior, just larger.
    • Tender slide (existing): unchanged — CSS-driven during takeover (not part of rotation).
  • Footer (~7% of viewport): tender + serving hours + Now Playing one-liner + refresh indicator + AT logo. Same as today.

Phase 2 addition (separate design + commits, see “Phase 2: Now Playing Hero” section below).

Implementation shape — Phase 1

New files:

  • public/bar-tv-left/index.html — header + .tv-left panels (featured + announcements) + footer. Includes the mobile-blocker div (carried over verbatim — same dev-protection role). Includes the takeover-boot-substate restoration <script> block (lines 26-38 of public/tv/index.html) verbatim.
  • public/bar-tv-right/index.html — header + .tv-aside contents (right-rail-stage with overview + flyer + tender slides) + footer. Same mobile-blocker and takeover-boot-substate script.

Asset paths (load-bearing):

  • Both new HTMLs reference styles.css and script.js with absolute paths: <link rel="stylesheet" href="/tv/styles.css" /> and <script src="/tv/script.js"></script>. No file duplication. Avoids the relative-path 404 trap (/bar-tv-left/styles.css doesn’t exist).
  • The version-rewrite regex in buildTvAssetResponse must be updated to handle absolute paths (see worker.js changes below).

Shared, with additions:

  • public/tv/styles.css becomes shared across all three routes. Add body-class scoping: body.bar-tv-left { ... } and body.bar-tv-right { ... } for side-specific sizing. The /tv route keeps its current body class (or no class) to preserve existing rendering — backwards compatible.
  • public/tv/script.js continues to be the single rendering script. Spot-check verdict from review: renderFeaturedEvent, renderWeekEvents, renderSlideshow, renderPubHoursWeek, showRightRailSlide all early-return on missing selectors. showRightRailSlide requires both overview and flyer — present on right TV, absent on left TV (where it bails cleanly). setupAutoScrollers targets featured-body (left) and week-list (right) — each TV has one, neither has both. Confirm in implementation, but no architectural blocker.
  • Now Playing SSE init gate (script.js:3478): wrap the new EventSource(NOW_PLAYING_SSE_URL) call in an element-presence check. Open the SSE only if #footer-now-playing is visible OR a future hero slot exists. LEFT TV skips the connection entirely (footer line is suppressed via CSS, no hero in Phase 1).

worker.js changes (critical):

  1. Route the new HTMLs. Mirror the existing /tv/tv/ redirect pattern so remote-typed URLs without trailing slashes still work:
    if (url.pathname === "/bar-tv-left" || url.pathname === "/bar-tv-left/index.html") {
      return Response.redirect(new URL("/bar-tv-left/", url.origin).toString(), 302);
    }
    if (url.pathname === "/bar-tv-right" || url.pathname === "/bar-tv-right/index.html") {
      return Response.redirect(new URL("/bar-tv-right/", url.origin).toString(), 302);
    }
  2. Extend buildTvAssetResponse invocation. The check at line 395 must also fire for the new paths:
    if (
      url.pathname === "/tv/" || url.pathname === "/tv/index.html" ||
      url.pathname === "/bar-tv-left/" || url.pathname === "/bar-tv-left/index.html" ||
      url.pathname === "/bar-tv-right/" || url.pathname === "/bar-tv-right/index.html"
    ) {
      return buildTvAssetResponse(response, env, url);
    }
    This is what guarantees:
    • window.__TV_BUILD_ID__ / window.__TV_DEBUG_MODE__ are injected → the build-poll self-reload pipeline keeps working.
    • No-cache headers are set → stale Tizen WebKit doesn’t pin old assets.
  3. Add two extra .replace() calls in buildTvAssetResponse to handle absolute paths. Existing relative-path regex stays untouched (zero risk to /tv):
    const versionedHtml = html
      // existing — unchanged
      .replace(/href="styles\.css(?:\?v=[^"]*)?"/i, `href="styles.css?v=${assetVersion}"`)
      .replace(/src="script\.js(?:\?v=[^"]*)?"/i, `src="script.js?v=${assetVersion}"`)
      // new — for /bar-tv-left and /bar-tv-right
      .replace(/href="\/tv\/styles\.css(?:\?v=[^"]*)?"/i, `href="/tv/styles.css?v=${assetVersion}"`)
      .replace(/src="\/tv\/script\.js(?:\?v=[^"]*)?"/i, `src="/tv/script.js?v=${assetVersion}"`);
    Both new patterns are idempotent (re-running yields the same output).

/api/tv-health identification:

  • tvHealthState.screenLabel already exists in the ping payload (script.js:91-94) but is empty today. Set it at init from window.location.pathname (e.g., tvHealthState.screenLabel = window.location.pathname.replace(/\//g, "") || "tv"). Two-line change to script.js. No worker.js / R2 / admin-dashboard changes required.

Takeover coordination:

  • Both TVs subscribe to the same takeover SSE/state. Body class event-takeover-pre / event-takeover-live applies on both.
  • LEFT TV: featured panel hides; mosaic (#event-mosaic) takes the full LEFT TV viewport (spans both featured + slideshow zones via takeover CSS scoped under body.bar-tv-left.event-takeover-live).
  • RIGHT TV: right-rail-stage swaps to right-rail-tender slide as it does today (CSS-driven, no JS rotation change needed). Becomes the “who’s serving right now during this special event” surface.
  • The current body.event-takeover-pre .tv-left / .tv-aside CSS at styles.css:1646+ needs side-scoped equivalents.

Failure modes & graceful degradation

  • One TV powered off or showing stale build: the other still renders a complete, useful screen. No layout depends on cross-TV state.
  • Tizen build-update reload mid-rotation: each TV recovers independently via window.__TV_BUILD_ID__ polling. The takeover-boot-substate restoration script is copied into both new HTMLs.
  • Right TV under takeover with no active shift: existing #right-rail-tender rendering already handles “no tender” — unchanged.
  • Stale CSS rule from /tv selectors leaking into new pages: scope all new CSS under body.bar-tv-left / body.bar-tv-right. Audit existing CSS for selectors that target .tv-left / .tv-aside unconditionally and verify they still produce the right result when those classes are nested under the new body classes (they should — selectors are additive).

Phase 2: Now Playing Hero (separate design, deferred)

Once Phase 1 is shipped and the readability win is banked, layer in the Now Playing hero. Why this is its own thing:

  • showRightRailSlide currently toggles only between #right-rail-overview and #right-rail-flyer. Adding a third rotation slot requires extending showRightRailSlide, buildRightRailSlides, and the slide-type union.
  • rightRailSlideFingerprint (used to detect when to re-sync rotation) needs to incorporate Now Playing state so a track change re-triggers rotation correctly.
  • renderNowPlaying today only targets #footer-now-playing. A new render function for the hero card is needed.
  • Cadence question is open: rotate the hero in/out, or pin it while a song plays?
  • Footer/hero redundancy question is open: hide the footer line when the hero is visible, or always show both?

Recommend writing a focused Phase-2 design doc after Phase 1 has been live for ~1 week. By then you’ll have observed how the new layout feels and whether Now Playing in the footer is enough or genuinely cramped.

Cross-Model Perspective

Cross-model second opinion skipped during alternatives selection. However, the doc was run through an adversarial spec review (independent agent, fresh context). Key findings caught and folded back into this doc:

  • The worker.js buildTvAssetResponse integration is load-bearing — without extending it to the new paths, the new TVs would silently stop receiving __TV_BUILD_ID__ injection and stale-cache prevention, regressing the recent 45-49 work.
  • Relative styles.css / script.js paths would 404 from new routes — switched to absolute.
  • Now Playing hero involves real refactoring of showRightRailSlide + buildRightRailSlides + rotation reactivity — pulled out into Phase 2.
  • /tv redirect pattern needs to be mirrored for the new routes so remote-typed URLs work without trailing slashes.
  • TV-health dashboard can’t distinguish the two TVs without a pathname in the ping payload.

Independent review by /codex review is recommended once the Phase 1 implementation diff exists.

Empty / sparse states (locked during /plan-design-review)

  • LEFT TV with no Featured Event: layout stays consistent. Hero shows a warmer empty state: I-House emblem + tagline (something like “Welcome to the pub. Tonight’s chill.“) + “check back later” copy. Slideshow does NOT grow. Implementer reference: a 6vw warm copy line, centered, with the emblem at ~10vw above.
  • RIGHT TV with 0 or 1 upcoming events: layout stays consistent. Coming Up shows a warmer line: “It’s a quiet week. Pub still serving every night.” Pub Hours panel stays at ~45% height. Layout does NOT shift.
  • LEFT TV during is-image-slide slideshow takeover: photo full-bleed across the slideshow zone at full LEFT TV width. Featured Event panel stays visible above (doesn’t hide). Image-only slides are the cinematic moment within the rotation, not a layout swap.

Mosaic takeover (locked during /plan-design-review)

Ship Phase 1 with the current mosaic engine scaled up to full LEFT TV viewport. Tiles get larger; tile count unchanged. After Phase 1 ships, photograph an actual takeover at the pub and run /design-shotgun or /design-review to evaluate engine improvements with real visuals. Mosaic engine refactor is its own TODO.

Open Questions

Phase 1:

  1. Featured Event empty state on LEFT TV. When there’s no featured event scheduled, does the featured hero shrink and the slideshow expand to fill, or does the hero stay with an “Empty” placeholder for visual consistency? Locked: layout consistent, warmer empty state copy. See “Empty / sparse states” above.
  2. Sub-events under Featured Event. Today the featured panel can list multiple events for a day (featured-also). At full-TV scale, do they get equal hero treatment, or stay as supporting text below the primary event? Suggestion: supporting text below — preserves the “this is the headliner” feel.
  3. Mosaic during takeover on LEFT TV. Locked: span both Featured + Slideshow zones (full LEFT TV viewport). Mosaic engine improvements deferred to post-Phase-1 design exploration with real takeover photos.
  4. /tv long-term. Once both TVs are pointed at the new routes, does /tv get deprecated, kept as a dev preview (laptop-friendly half-width), or redirect? Probably: keep as dev preview indefinitely. Costs nothing.
  5. Pub Hours grid fits in full TV at bumped fonts? Today the 7-day grid auto-scrolls if it overflows. At full-TV scale with bigger fonts, does the auto-scroller still kick in or does it fit cleanly? Verify on real Tizen, not just laptop.

Phase 2 (deferred to Now Playing hero design):

  1. Now Playing rotation cadence — rotate hero in/out or pin while playing?
  2. Footer Now Playing line + hero card simultaneously — both visible, or suppress footer when hero is on screen?
  3. Album art availability — is the song-ID pipeline returning art URLs today, or text-only?

Success Criteria — Phase 1

  • A patron seated at the back booth (furthest point in the bar from the TVs) can read:
    • The Featured Event title and time without squinting.
    • The next 2-3 upcoming events from Coming Up.
    • The Pub Hours grid for at least today + tomorrow.
  • The two TVs read as a coherent single surface, not as “two random screens.”
  • Manual remote-driven URL refresh works on either TV in under 30s.
  • Special event takeover still triggers correctly on both TVs.
  • A patron at Bar Right who can’t see the left TV (e.g., obstructed) still gets useful planning info from the right TV alone.
  • A patron at Bar Left who can’t see the right TV still gets useful “now” info.
  • TV_BUILD_ID self-reload pipeline continues to work on the new routes (deploy a build bump and confirm Tizens reload within their poll window).
  • The /api/tv-health dashboard can distinguish the two TVs.

Phase 2 adds: patron at the back booth can read the current Now Playing track without footer-squinting. Deferred to its own design.

Distribution Plan

Existing Cloudflare Worker deployment pipeline covers this. No new infrastructure:

  • New static HTML files under public/bar-tv-left/ and public/bar-tv-right/ are served via the existing ASSETS binding.
  • worker.js route additions ship with the next worker deploy.
  • Bump TV_BUILD_ID so Tizens reload the new script + styles when the bookmarks are swapped.
  • Operator action to actually point the TVs at the new URLs: walk to each TV with the remote, open the browser, navigate to the new URL, save as bookmark/home. ~2 minutes per TV.

Dependencies

  • Blocking: Phase 0 (vitest+miniflare bootstrap, TODOS.md #1) must ship before Phase 1. Decision locked during /plan-eng-review on 2026-05-20.
  • The song-ID pipeline is already shipped (commits 43-49), so Phase 2’s Now Playing hero will have its data source on day one.
  • The special-event takeover system is already shipped (SPECIAL_EVENT_TAKEOVER_PLAN.md).
  • No new runtime services. Phase 0 adds vitest, miniflare, @cloudflare/workers-types as devDependencies.

Next Steps — Phase 0 (prerequisite PR)

Before Phase 1 lands, bootstrap a test framework. From TODOS.md #1:

  1. Add vitest + @cloudflare/workers-types + miniflare to the project. Create package.json, basic vitest.config.ts, one smoke test against worker.js confirming /tv/ returns 200. Ships as its own PR. ~3-4 hours.

Next Steps — Phase 1 (after Phase 0 ships)

Concrete build order (each is a small commit):

  1. Carve out the HTMLs. Copy public/tv/index.html twice into public/bar-tv-left/index.html and public/bar-tv-right/index.html. Strip each to its side’s panels. Both keep header, footer, mobile-blocker, and takeover-boot-substate restoration script. Switch styles.css / script.js references to absolute paths (/tv/styles.css, /tv/script.js).
  2. Add body classes. <body class="bar-tv-left"> and <body class="bar-tv-right">. These scope side-specific sizing.
  3. Wire worker routes. In worker.js:
    • Add /bar-tv-left/bar-tv-left/ and /bar-tv-right/bar-tv-right/ redirects (mirror the existing /tv redirect).
    • Extend the buildTvAssetResponse call site to also fire on the new paths.
    • Add the two new .replace() calls inside buildTvAssetResponse for the absolute /tv/styles.css and /tv/script.js paths (do not modify the existing relative-path lines).
  4. Add unit tests. Tests for the four buildTvAssetResponse regex behaviors (relative+absolute × styles+script) + miniflare integration tests for the four new redirects + 200-OK on both new paths + regression test that GET /tv/ still works unchanged.
  5. Verify script.js selector safety on each side. Quick smoke-test by loading each new HTML on a laptop. Watch the console for null-ref errors. Add if (!el) return; guards on any path that fires.
  6. Gate Now Playing SSE init on element presence. In script.js, before new EventSource(NOW_PLAYING_SSE_URL), check that a Now Playing target exists in the DOM. If not, skip. LEFT TV skips, RIGHT TV connects.
  7. Set tvHealthState.screenLabel from pathname at init. Two-line change in script.js so the existing health dashboard distinguishes the two TVs without any backend changes.
  8. Sizing pass — LEFT TV. New CSS rules under body.bar-tv-left: Featured Event hero gets ~5-7vw title font, full-TV-width slideshow, photo full-bleed treatment in is-image-slide mode.
  9. Sizing pass — RIGHT TV. New CSS rules under body.bar-tv-right: Coming Up + Pub Hours scaled up significantly. Verify the auto-scroller behavior on a real 7-day week.
  10. Takeover behavior verification. Trigger a test takeover and confirm both TVs swap correctly. Scope LEFT TV mosaic to span the full viewport via body.bar-tv-left.event-takeover-live CSS.
  11. Bump TV_BUILD_ID. Push the worker + assets. Existing /tv keeps working unchanged.
  12. Swap TV bookmarks in pub. Walk over with the remote, point each TV at its new URL. Save as bookmark/home. ~2 min per TV.
  13. Live observation. Stand at the back booth. Confirm readability. Watch for one full rotation cycle on each side. Note anything cramped.

Next Steps — Phase 2 (after Phase 1 ships)

Out of scope for this design. Write a focused design doc for the Now Playing hero once Phase 1 has run live for ~1 week.

NOT in scope (Phase 1)

  • Now Playing hero card on RIGHT TV. Deferred to Phase 2 (separate design). Requires extending showRightRailSlide, buildRightRailSlides, and rotation reactivity — non-trivial.
  • Album art for Now Playing. Depends on the song-ID pipeline returning art URLs; out of scope until Phase 2.
  • /tv deprecation. Stays as dev/laptop preview indefinitely. No redirect.
  • TV-health dashboard UI updates. No worker.js or admin-dashboard changes; the existing screenLabel field carries the new identification.
  • Eval/sync between LEFT and RIGHT slideshow timing. Each TV’s rotation is independent. Not coordinated.
  • Panoramic-photo / cross-TV layouts (Approach C from the alternatives). Out of scope. Possibly forever.

What already exists

The plan reuses, doesn’t rebuild:

  • /tv page architecture — header + .tv-left + .tv-aside + footer. Each new HTML is a carve-out of the existing structure.
  • script.js rendering logic — every render* entry point already early-returns on missing selectors. No new render code in Phase 1.
  • buildTvAssetResponse versioning + __TV_BUILD_ID__ injection — extended to two new path conditions; same function, same behavior.
  • tvHealthState.screenLabel — field already exists in the ping payload (script.js:91-94), currently empty. Phase 1 sets it from pathname.
  • pollLatestBuild self-reload pipeline — works on the new pages once __TV_BUILD_ID__ is injected. Zero new code.
  • event-takeover-pre / event-takeover-live body class system — both pages enter takeover state via the existing events fetch path.
  • Mobile-blocker + takeover-boot-substate restoration <script> — copied verbatim into both new HTMLs.

Failure modes

CodepathRealistic failureTest coverageError handlingUser-visible?
buildTvAssetResponse regex (new absolute paths)Pattern doesn’t match → assets never get ?v=... → Tizens pin stale build foreverUNIT TEST (Phase 0)None — silentYes, eventually (stale UI never updates)
/bar-tv-left/bar-tv-left/ redirectMissing redirect → Tizen remote types path without slash → 404INTEGRATION TEST (Phase 0)404 by ASSETS bindingYes (operator sees 404)
__TV_BUILD_ID__ injection on new pathsbuildTvAssetResponse not called → window.__TV_BUILD_ID__ undefined → pollLatestBuild returns nullINTEGRATION TEST (Phase 0)Defensive null check existsSilent — TV never self-reloads
Now Playing SSE element-presence gateGate misses, SSE opens on LEFT TVManual verification (DevTools Network)Idempotent, no UI impactNo
LEFT mosaic takeover scopingCSS rule scoped to old .tv-aside → mosaic doesn’t span on LEFTManual takeover testNoneYes (mosaic looks cramped)
Pub Hours grid overflow on full-TV RIGHTBumped font pushes 7-day grid off-screenManual back-booth checkExisting auto-scroller covers itYes (rows scroll, may miss days)

Critical gaps flagged: The first row (regex pattern miss) is the only failure mode that is silent + has no error handling AND would cause a user-visible regression. Phase 0’s unit test for buildTvAssetResponse is the mitigation. This is exactly the test that justifies the test-framework investment.

Worktree parallelization strategy

Sequential implementation, no parallelization opportunity. Phase 0 must ship before Phase 1, and Phase 1 steps share worker.js, styles.css, and script.js — same files, sequential commits. No independent lanes.

What I noticed about how you think

  • You came in with “I want to think through it fully” and then immediately refined two of the four premises I floated. That’s a tell — you don’t take framings on faith, you adjust them. The premise-1 refinement (“Yes it’s a fix of layout, but also I’d like to consider content targeting also”) is what unlocked Approach B over Approach A. Without that nudge I’d have shipped you the minimum-viable version.
  • You noticed the ?v=... build-bump query param without me prompting, and used it to sanity-check the “two URLs vs. one URL with query param” decision. That’s the kind of detail most people miss when they’re choosing an architecture — they only notice the conflict when they hit it in code. You caught it at the design stage.
  • The operator-pragmatism check (“if I needed to manually use the remote and refresh the TVs”) shows you’re designing for the actual physical workflow, not the idealized one. URLs that are bookmarkable and typeable are a real constraint, and you named it before I had to.
  • You’re treating the existing codebase as a system that’s been earning its keep — every prior decision (takeover system, song-ID pipeline, build versioning) is being built ON, not replaced. That’s why the alternatives section is short on “rebuild” options: you don’t need them.

GSTACK REVIEW REPORT

ReviewTriggerWhyRunsStatusFindings
CEO Review/plan-ceo-reviewScope & strategy0
Codex Review/codex reviewIndependent 2nd opinion0skipped (small scope)
Eng Review/plan-eng-reviewArchitecture & tests (required)1CLEAR (PLAN)5 issues, 1 critical gap (asset-version regex miss → mitigated by Phase 0 unit tests)
Design Review/plan-design-reviewUI/UX gaps1CLEAR (FULL)score: 6/10 → 9/10, 7 decisions locked (hierarchy, type scale, empty states, mosaic deferred)
DX Review/plan-devex-reviewDeveloper experience gaps0
  • UNRESOLVED: 0
  • VERDICT: ENG + DESIGN CLEARED — ready to implement (Phase 0 first, then Phase 1)