Overview

  • Full-screen 16:9 dark-theme signage board at /tv/
  • Designed for in-venue TV screens and blocks mobile viewports with a friendly message
  • Pulls live data from /api/events every 2 minutes
  • Uses the API response’s lastUpdated timestamp as the shared clock anchor for slideshow rotation and featured-event conflict rotation
  • Visible header clock is still device-local time
  • Caches the last successful /api/events payload in localStorage so reloads can repaint immediately from prior data
  • Polls /api/tv-build every minute and reloads only when the deployed TV build changes
  • TV_BUILD_ID is injected dynamically by the worker as window.__TV_BUILD_ID__; if the TV loads a cached HTML page, the build ID self-corrects from the first poll result
  • Hard-refresh fallback every hour via <meta http-equiv="refresh">
  • /tv/ can show a device-side debug overlay when TV_DEBUG_MODE = "1" is injected by the worker

Sources: public/tv/index.html, public/tv/script.js, public/tv/styles.css

Live Screenshots

The TV is easier to understand from screenshots than from the DOM alone. These captures were taken from the live production page at pub.ihnyc-rc.org/tv on April 4, 2026.

Live TV overview showing the full screen layout

  • Left column: Happening at the Pub for same-day featured activity and the mixed announcement / tender slideshow beneath it
  • Right column: Coming Up at the Pub for tomorrow-through-next-7-days events and Pub Serving Hours This Week for the current weekly service grid
  • Footer: current tender assignment, serving window, shift note, refresh countdown, and environment flag
  • In this live frame, Happening at the Pub is populated by an overnight event that began the previous evening and is still active after midnight

Live Happening at the Pub panel

  • This panel is driven from todayEvents and now correctly includes overnight events that overlap the current Eastern calendar day
  • The green Now Happening badge is the highest-priority featured state and is what pub staff and guests should notice first

Live announcement slide area

  • Announcement slides can be text, image, or mixed image+text
  • The slideshow dots indicate the current slide and give manual navigation targets
  • When a full image announcement is active, the slide can use most of the lower-left panel

Live upcoming-events and pub-hours column

  • Coming Up at the Pub is optimized for quick scanning of the next several pub events
  • Pub Serving Hours This Week highlights today’s service state while still showing the rest of the week
  • This right-side column is the highest-density “what is going on here?” area for people glancing at the TV

Layout

+-----------------------------------------------------+
| Header: logo, current date, live clock              |
+------------------------------+----------------------+
| Left column                  | Right column         |
| - Happening at the Pub       | - Coming Up at the   |
| - Announcements / tender     |   Pub                |
|   slideshow                  | - Pub Hours This     |
|                              |   Week               |
+-----------------------------------------------------+
| Footer: tender, hours, note, refresh, test flag     |
+-----------------------------------------------------+

Sources: public/tv/index.html, public/tv/styles.css (.tv-main, .tv-left, .tv-aside)


Event Inclusion Pipeline

The TV page does not query Notion directly. It renders whatever the worker returns from GET /api/events.

Both event arrays use the same base gate:

  • Include on Pub TV must be checked (location is no longer filtered — any location or none is accepted)
  • Events are sorted by actual start timestamp before the API response is returned

todayEvents

  • The worker computes todayISO in America/New_York
  • The event day starts at 12:00 AM Eastern
  • The worker queries a broad Notion window from todayISO through < todayISO + 2 days
  • It then narrows that result set to rows whose Event Start falls on todayISO in Eastern time

weekEvents

  • Uses the same pub-location and TV-include gates
  • Pulls rows with Event Start >= tomorrowISO and < todayISO + 8 days
  • That means calendar tomorrow through the next 7 days

Sources: worker.js (fetchFromNotion, eventOverlapsLocalDate, transformPage, transformWeekPage)


Event Status Logic

Each event is evaluated client-side using ISO timestamps from the API and the TV’s server-aligned clock.

StatusConditionBadge
NowstartMs <= now < endMsNow Happening
NextStarts within 90 minutesComing Up Next
LaterStarts more than 90 minutes from nowComing Soon
DoneEnd time has passedHidden
  • The Featured Event panel shows the highest-priority non-Done event (Now > Next > Later)
  • When multiple events share the same timeslot and status, the panel rotates among them every 15 seconds using the shared server-aligned clock
  • The Coming Up list filters out any Done rows before rendering
  • If an event has no usable end timestamp, the client falls back to start + 1 hour

Sources: public/tv/script.js (deriveStatusFromEvent, selectFeaturedEvent, renderWeekEvents)


Happening at the Pub

This panel renders from todayEvents only.

  1. The client re-derives every event’s live status from startIso and endIso
  2. It picks a single featured row using priority order Now Next Later
  3. It renders the featured title, RC badge, time range, and location (if set)
  4. It then lists the remaining non-Done same-day events under Also today, each with title, time, and location

If there is no non-Done same-day event, the panel stays visible and shows the empty-state message.

Sources: public/tv/script.js (withLiveStatuses, selectFeaturedEvent, renderFeaturedEvent)


RC Classification Badges

Events can carry one of two RC classification tags, shown as a badge before the event title.

TagBadge styleMeaning
rc-directSolid greenOrganized by an RC member
rc-affiliateOutlined accentResident-led, supported by the RC
  • Badge displays the RC logo (RC-transparent-background-logo.png)
  • Tag is extracted from the event row’s Source Email Subject by matching RC-Direct or RC-Affiliate

Sources: public/tv/script.js (rcBadgeHtml), worker.js (extractRcTag)


Announcements and Meet your Tender Slideshow

The bottom-left panel is a slideshow that rotates through two types of slides:

  1. Announcement slides
  2. Tender profile slides

Slides auto-advance according to each slide’s configured duration, defaulting to 10 seconds. Navigation dots are shown inline in the panel heading bar, right-aligned after the slide heading text (e.g. “Announcements ●●○”). They are hidden when only one slide is present.

The slide position is no longer session-relative. Each TV computes the active slide from:

  • the shared slides payload
  • each slide’s duration
  • the API response’s lastUpdated timestamp

That keeps multiple TV sessions aligned even if browsers were opened at different times, as long as they are on the same payload.

Announcement slides

  • Can be text-only, image-only, or mixed image+text cards
  • Show a title (Name) and body text (Content, falling back to Name)
  • Can render an image from the announcement page’s Picture, Image, or PNG file property
  • Use Duration when present to override the default 10-second slide duration
  • Use the announcement priority field as a styling hint
  • Image URLs are rewritten to same-origin /api/image/announcement/:pageId routes before the frontend sees them
  • Those image routes can mirror Notion-hosted files into R2, so always-on TV sessions are less dependent on temporary Notion file URLs remaining valid
  • Pure image slides collapse the featured-event panel into a compact strip so the announcement image can use more vertical space
  • Fall back to a single empty-state message if the combined slides array is empty

Tender profile slides

  • Heading reads “Meet your Tender”
  • Show the tender’s first name only
  • Show the tender’s Picture through a same-origin /api/image/tender/:pageId route
  • Show a QR image only when the QR file property resolves successfully, via /api/image/tender-qr/:pageId
  • Do not currently show the tender bio, even if Bio exists on the Notion page
  • Do not generate a QR in the browser. The TV only renders the pre-generated Notion QR image file
  • Are only generated when Meet your Tender is checked on the tender page

Tender slide layout

The tender card (.slide-tender-layout) uses position: absolute with explicit inset values rather than flex sizing. Flex-based percentage-height resolution through the position: absolute slideshow chain was unreliable on certain TV rendering environments. The inset values map directly to .tv-slide’s edges and are deterministic on all screens.

QR code destination

The QR code displayed in the tender slide encodes the Short.io short URL (e.g. go.affectivetech.com/pub-pt3). That short URL’s target is the canonical profile URL /tenders/?tender=<id>. The older /tender/:id route still exists only as a compatibility redirect into the query-based directory URL.

Sources: public/tv/script.js (renderSlideshow, renderSlideMarkup), worker.js (buildSlides, fetchTenderInfoFromAssignmentPage)


Coming Up Panel

  • Renders from weekEvents, which the worker defines as calendar tomorrow through the next 7 days
  • Same-day events are intentionally excluded
  • Events whose computed status is Done are removed before rendering
  • Long event titles animate with a horizontal marquee scroll
  • The panel is hidden entirely if no upcoming events remain

Sources: public/tv/script.js (renderWeekEvents, startWeekTitleScroll)


Pub Hours Grid

  • Seven-column grid showing Sunday through Saturday pub hours for the current week
  • Today’s column has three visual states:
    • amber before today’s first shift segment starts, or between multiple segments
    • green while a shift segment is active
    • dimmed after the last segment has ended
  • Overnight segments like 9:00 PM - 1:40 AM are treated as ending on the next day when determining those states
  • Shift window math is evaluated explicitly in America/New_York, not the browser’s local timezone

Sources: public/tv/script.js (renderPubHoursWeek, getShiftStateForDate, getEffectivePubHoursDate)


Persistent footer bar showing the on-duty tender and live shift state.

FieldContent
Pub TenderFirst name(s) of on-duty tender(s)
Serving HoursOperational timeframe
Shift StateLive note with color and animation

Footer vs profile slide

The footer always shows the tender’s first name, regardless of whether Meet your Tender is checked. The checkbox only gates the profile slide in the slideshow panel.

Shift state transitions

StateNote textVisual
upcomingUpcoming shiftNormal
setupSetting upAmber
activeOn duty nowNormal
last-callLast callRed, pulsing
closedCleaning / closedDimmed
inactiveNo active shift right nowNormal

Sources: public/tv/script.js (renderPubTender), worker.js (deriveShiftNote)


Refresh Strategy

MechanismIntervalPurpose
Featured event re-evaluation15 seconds, client-sideKeeps badges and same-timeslot rotation accurate between API calls
Soft data refresh2 minutes via /api/eventsPulls fresh event, announcement, and tender-footer data
Build fingerprint poll1 minute via /api/tv-buildReloads only when deployed TV assets actually changed
Hard meta refresh fallback1 hourSafety net if JavaScript hangs completely
Countdown displayLiveShows Refreshing in M:SS in the footer
Slideshow rotationDuration-driven, default 10 secondsAdvances slides using the shared server-aligned cycle

Sources: public/tv/script.js (init, startRefreshCountdown), public/tv/index.html (<meta http-equiv="refresh">)


Debug Mode

Set TV_DEBUG_MODE = "1" and open /tv/ to show a device-side overlay with:

  • /api/events fetch status
  • server timestamp used for sync
  • active slide index and type
  • image load vs error status
  • exact current image URL
  • viewport size, DPR, and user agent
  • unhandled JS/runtime errors

This mode exists specifically to debug embedded TV browsers that behave differently from desktop browsers.

Sources: public/tv/script.js (TV_DEBUG_MODE, installDebugHooks, startBuildPolling), worker.js (buildTvAssetResponse, handleTvBuildApi), public/tv/styles.css (.tv-debug-overlay)



iPad Night Board (/ipad/)

A separate landscape-only board at /ipad/ designed to sit on a bar-top iPad during pub events.

What it shows

  • Tonight’s Events — all todayEvents returned by the API, each row showing: status pill (Now / Next / Later / Done), event title, and time + location inline (7:00 PM – 9:00 PM · 📍 Pub)
  • On Shift Tonight — photo(s) and full name(s) of active pub tenders from pubTender.activeTenders

What it does not show

  • No hero/featured event panel
  • No announcement slideshow
  • No “Coming Up” week list
  • No board-status meta (refresh cadence, source, last sync)
  • No tender shift notes, hours, or detail cards

Layout

+---------------------------------------------+
| Logo  Tonight at the Pub  [date]     [clock] |
+----------------------------+----------------+
| Tonight's Events           | On Shift       |
|                            | Tonight        |
| [Now]  Trivia Night        |                |
|        8:30 PM · 📍 Pub   |  [photo]       |
|                            |  First Last    |
| [Next] Karaoke             |                |
|        10:00 PM            |                |
+----------------------------+----------------+

Event row ordering and states

  • Active events (Now, Next, Later) are always shown first
  • Done events are sorted to the bottom, faded to 45% opacity
  • If all events are done, the list collapses to a single “All wrapped up for tonight” message — no need to show a wall of faded rows

Event row accent

  • The active Now row gets a green left-border accent, a subtle green tint, and a slow breathing glow animation so bar staff can spot the current event without reading the pill
  • All rows carry a 3px left border; only Now has a colored one

Refresh

  • Data: every 60 seconds via /api/events?window=1
  • Clock: every 10 seconds client-side
  • Status re-evaluation: every 15 seconds client-side
  • Hard page reload: every 5 minutes via <meta http-equiv="refresh">
  • Portrait orientation on a touch device shows a rotate prompt and hides the board

Rotation blocker

If the iPad is held in portrait, a full-screen overlay asks the user to rotate. The board content is hidden until landscape orientation is restored.

Sources: public/ipad/index.html, public/ipad/script.js, public/ipad/styles.css