System Summary

  • Single Cloudflare Worker (worker.js) handles all requests for pub.ihnyc-rc.org
  • Static files are served from ./public via Cloudflare’s Assets binding with SPA-style fallback for unknown asset paths
  • /api/events queries the events, shifts, and announcements data sources and assembles a unified payload for the landing page, announcements page, and TV board
  • /api/tenders returns the active tender directory, either from an explicit Pub Tenders DB id or by discovering the tender database through recent shift assignments
  • /api/image/:kind/:pageId resolves short same-origin image URLs and can mirror Notion-hosted images into R2 so TV/announcement surfaces do not depend on long-lived access to signed Notion file URLs
  • /api/sync-tender-qrs is an authenticated admin route that creates or updates Short.io tender links and writes the resulting QR SVG into Notion
  • /tender/:id is a compatibility redirect into the modal-backed /tenders/ experience
  • API responses are cached at the edge for 2 minutes (CACHE_TTL_SECONDS = 120)
  • /tv/ itself is served with no-store headers so screens are less likely to stay on stale frontend assets
  • No build step; plain HTML, CSS, and JavaScript for all static pages

Sources: worker.js, wrangler.toml


Request Routing

flowchart TD
  REQ["Incoming Request"] --> WORKER["Cloudflare Worker (worker.js)"]
  WORKER --> ROUTE{Route type?}

  ROUTE -- "/api/events" --> EVENTS_CACHE{Edge Cache Hit?}
  EVENTS_CACHE -- Yes --> EVENTS_CACHED["Return Cached /api/events"]
  EVENTS_CACHE -- No --> EVENTS_NOTION["Query Notion APIs"]
  EVENTS_NOTION --> EVENTS_ASSEMBLE["Assemble event payload"]
  EVENTS_ASSEMBLE --> EVENTS_STORE["Store in edge cache (2 min TTL)"]
  EVENTS_STORE --> EVENTS_RETURN["Return JSON"]

  ROUTE -- "/api/tenders" --> TENDERS_CACHE{Edge Cache Hit?}
  TENDERS_CACHE -- Yes --> TENDERS_CACHED["Return Cached /api/tenders"]
  TENDERS_CACHE -- No --> TENDERS_NOTION["Query or discover tender DB"]
  TENDERS_NOTION --> TENDERS_ASSEMBLE["Assemble tender directory"]
  TENDERS_ASSEMBLE --> TENDERS_STORE["Store in edge cache (2 min TTL)"]
  TENDERS_STORE --> TENDERS_RETURN["Return JSON"]

  ROUTE -- "/api/image/:kind/:pageId" --> IMAGE_R2{"R2 mirror hit?"}
  IMAGE_R2 -- Yes --> IMAGE_R2_RETURN["Return mirrored image from R2"]
  IMAGE_R2 -- No --> IMAGE_RESOLVE["Resolve Notion file URL"]
  IMAGE_RESOLVE --> IMAGE_FETCH["Fetch upstream image"]
  IMAGE_FETCH --> IMAGE_STORE["Store bytes in R2 mirror"]
  IMAGE_STORE --> IMAGE_RETURN["Return proxied image"]

  ROUTE -- "/api/sync-tender-qrs" --> QR_AUTH{Authorized POST?}
  QR_AUTH -- No --> QR_DENY["401 / 405"]
  QR_AUTH -- Yes --> QR_SYNC["Sync Short.io link + SVG back to Notion"]
  QR_SYNC --> QR_RETURN["Return JSON summary"]

  ROUTE -- "/tender/:id" --> REDIRECT["302 redirect to /tenders/?tender=:id"]
  ROUTE -- "Static asset or page" --> ASSETS["Cloudflare Assets (./public)"]
  ASSETS --> FOUND{Asset found?}
  FOUND -- Yes --> SERVE["Serve static file"]
  FOUND -- No --> DIRCHECK{Slashless directory page?}
  DIRCHECK -- Yes --> DIRREDIRECT["302 redirect to trailing-slash path"]
  DIRCHECK -- No --> FALLBACK["Serve index.html (SPA fallback)"]

Sources: worker.js (fetch handler, CACHING section)


Notion Databases

Env VarPurposeRequired
NOTION_DATABASE_IDPub events for todayEvents and weekEventsYes for live events
NOTION_SHIFTS_DATABASE_IDShift windows, pub hours, footer state, and assignment traversalOptional
NOTION_ANNOUNCEMENTS_DATABASE_IDAnnouncement rows for the slideshow and /announcements/Optional
NOTION_PUB_TENDERS_DATABASE_IDExplicit tender directory database for /api/tendersOptional
  • NOTION_DATABASE_ID is required when live Notion data is enabled
  • If NOTION_API_KEY is absent, the worker skips live Notion queries and returns placeholder data
  • NOTION_API_KEY is required and is stored as a Wrangler secret
  • If NOTION_PUB_TENDERS_DATABASE_ID is missing, the worker attempts to discover the directory database by walking recent shift assignment tender relations

Sources: worker.js (CONFIG section), wrangler.toml


/api/events Payload Assembly

When /api/events is called, the worker:

  1. Determines “today” using the normal calendar date in America/New_York
  2. Queries the Events DB for todayEvents using a broad date window, then narrows it to rows whose local Eastern start date equals todayISO
  3. Queries the Events DB again for weekEvents covering calendar tomorrow through the next 7 days
  4. Queries the Announcements DB for TV-included announcements, if configured
  5. Rewrites announcement images to short same-origin /api/image/announcement/:pageId URLs
  6. Queries the Shifts DB for the active shift and current pub-tender footer state, if configured
  7. Rewrites tender photos and tender QR images to short same-origin /api/image/tender/:pageId and /api/image/tender-qr/:pageId URLs
  8. Synthesizes slides from announcements plus any active tender profiles marked Meet your Tender
  9. Returns lastUpdated, which the TV client uses as the shared server clock anchor
  10. Builds pubHoursWeek from the Shifts DB, if configured
  11. Returns a unified JSON object used by /, /announcements/, and /tv/

When an image route is requested, the worker can now use an R2 mirror bucket bound as IMAGE_MIRROR_BUCKET:

  • first request: resolve the current Notion file URL, fetch the bytes, store them in R2, then serve the image
  • later requests: serve the mirrored copy from R2 when it exists
  • stale mirrored objects are refreshed from Notion after IMAGE_R2_REFRESH_TTL_SECONDS
  • if the bucket binding is absent, the worker falls back to direct proxying plus edge caching only

Sources: worker.js (handleEventsApi, fetchFromNotion, fetchPubTenderFromNotion)


/api/tenders Payload Assembly

When /api/tenders is called, the worker:

  1. Resolves the Pub Tenders database id
  2. Uses NOTION_PUB_TENDERS_DATABASE_ID directly if it exists
  3. Otherwise inspects recent in-pub shifts and follows their assignment relations until it finds a tender page parent database
  4. Queries the full tender directory database
  5. Keeps only rows whose Active checkbox is true
  6. Maps each row into a lightweight directory card object for /tenders/

This lets the tender directory work even when the explicit Pub Tenders DB env var has not been set yet, as long as the shifts and assignments relations are healthy.

Sources: worker.js (handleTendersApi, fetchTenderDirectoryFromNotion, resolveTenderDirectoryDatabaseId)


Caching

  • Cache TTL: 2 minutes (CACHE_TTL_SECONDS = 120)
  • Cache key: full request URL
  • Cache stored in Cloudflare’s edge cache, not KV or D1
  • /api/image/* responses use browser and edge cache headers, and can use R2 as a durable origin mirror when IMAGE_MIRROR_BUCKET is configured
  • TV display client fetches /api/events with a timestamp query string and no-store request headers, then anchors shared slideshow and featured-event state to the response’s lastUpdated
  • /tv/ HTML is returned with Cache-Control: no-store, no-cache, must-revalidate
  • /tv/ soft-refreshes /api/events every 2 minutes, caches the last successful payload in localStorage, and repaints from that cache immediately after reload
  • /tv/ polls /api/tv-build every minute and reloads only when the deployed TV build fingerprint changes, preferably at a slide boundary
  • Hard <meta http-equiv="refresh" content="3600"> fallback every hour in case JavaScript hangs completely
  • Overnight serving-hours state on / and /tv/ is computed against America/New_York, not the device timezone

Sources: worker.js (CACHING section), public/tv/index.html, public/tv/script.js


Static Site Structure

public/
|-- index.html                 # Landing page with serving hours + nav
|-- styles.css                 # Shared styles used by several interior pages
|-- assets/                    # Logos and static imagery
|-- tv/
|   |-- index.html             # TV board shell
|   |-- styles.css             # TV-specific layout and motion
|   `-- script.js              # TV rendering and refresh logic
|-- announcements/             # Live announcements page
|-- tenders/                   # Modal-backed tender directory
|-- pub-tenders/               # Tender-facing static guide
|-- tutorials/
|   |-- index.html             # Tutorial index card grid
|   `-- bar-left-controls/     # Interactive bar-left A/V, lights, and HDMI help page
|-- menu/                      # Menu page
|-- specials/                  # Specials placeholder
|-- events/                    # Events placeholder
`-- resources/                 # Resources placeholder

Sources: public/ directory, wrangler.toml ([assets] binding)