Skip to content

Restore the two reverted D3 listener surfaces once the dj_name scrub lands (deferred past the turndown) #232

Description

@jakebromberg

Problem

Two of the three Phase 0 D3 successor pages were reverted off website main on 2026-08-26 because they render historical DJ identity that can hold DJs' real names. WXYC/website#230 decided on 2026-08-27 to accept that gap through the tubafrenzy turndown rather than slip Milestone 1 or ship the pages with identity stripped.

That decision settled the cutover. It did not schedule the rollforward, and #230 closed with the restoration recorded only as "reopen this issue then, or file a fresh one." This is that fresh one. Nothing else on org Project #36 currently represents the intent to bring these surfaces back.

Revert Page removed wxyc.info surface it succeeded Human req/day Original ticket
43c433d pages/airplay-search.jsx /playlists/searchPlaylists ~181 #212
90643dc pages/playlists/archive.jsx /playlists/radioWeek + its /playlists/radioShow drill-in ~1,546 + ~1,906 #213

wxyc.org/playlist (#211, succeeding /playlists/recent at ~224/day) is unaffected and stays up — it serves only recent entries, all written after the write-path fix.

Traffic figures are from the Phase 0 access-log measurement, bot traffic netted out. So the largest listener surface on wxyc.info after recentEntries is currently retiring with no replacement, and the smallest keeps one.

Timing — deliberately deferred, not stalled

The fixed rollforward is deferred until after tubafrenzy is switched off (Milestone 1, 2026-09-07, WXYC/wiki#93). Decided 2026-08-29. This ticket is After 9/7 — chain-gated on Project #36 and should not be pulled into the cutover.

Two reasons, both structural rather than scheduling preference:

  1. The blocker is Scrub historical denormalized dj_name values to match the current resolution policy Backend-Service#2281 — a ~2.6M-row batched migration over a table with a STORED GENERATED tsvector column, on which migration 0053 already wedged on locks once (Migration 0053 wedge: split DDL from backfill, harden migration runner Backend-Service#511). Running that against prod during the week a cutover lands is the wrong risk to stack.
  2. Milestone 1's own gate is DJ-account migration, not engineering (24 scheduled DJs still lack a usable Backend account — WXYC/wiki#125). Adding a public-surface restoration to that week competes for the same attention with no listener benefit until the scrub lands anyway.

Root cause (the exposure is in the data, not in these pages)

flowsheet.dj_name is a denormalized snapshot taken at play time, and projectFlowsheetEntry passes it through raw, so dj_join / dj_leave marker rows still carry whatever was resolved at write time. Show headings are exposed by a different route: resolveShowDjName's third step is shows.legacy_dj_name, backfilled from tubafrenzy and already holding real names for historical shows. resolveDjDisplayName is correct today — WXYC/Backend-Service#1286 removed auth_user.name from the chain — but a resolution helper being correct is not the same as the stored data conforming to it (the distinction WXYC/Backend-Service#2281 draws).

Sampling one 2015 week through GET /flowsheet/range: 10 of 46 distinct marker-row dj_name values and 12 of 50 show-heading values match a "Firstname Lastname" shape. That is a regex, so it counts two-word handles as hits and misses single-word real first names — it bounds the exposure as substantial, not exactly.

Consequence: any public page rendering historical DJ identity has this problem. Restoring these pages before the scrub would just move the exposure back.

Desired end state

Both pages are back on main and live at wxyc.org, rendering DJ identity that conforms to the current resolution policy, with the wxyc.info successor story whole again.

Where

Recoverable whole from the parent commits — this is a re-add, not a rewrite:

  • pages/airplay-search.jsx and __tests__/airplaySearch.test.jsx — at 43c433d^
  • pages/playlists/archive.jsx and __tests__/archivePlaylists.test.jsx — at 90643dc^

Both reverts deliberately left the data layer in place, so restoring is re-adding the page components alone:

  • lib/flowsheetSearch.js + __tests__/flowsheetSearch.test.js
  • lib/flowsheetRange.js + __tests__/flowsheetRange.test.js
  • components/OrganizingArchive.js

Also touched by both reverts and needing the inverse edit:

  • content/page/programming.mdx — the links to both pages were removed so the reverts left no dead links; re-add them.
  • __tests__/wxycInfoLinks.test.js — carries the deliberate-gap note; remove it when the gap closes. The turndown guard itself (no linking back to wxyc.info) stays.
  • README.md — both reverts removed the pages' sections.

Backend surfaces, both already live, public, and CORS-cleared for wxyc.orgno Backend work is in front of this beyond the scrub:

Constraints

  • Do not restore either page before Scrub historical denormalized dj_name values to match the current resolution policy Backend-Service#2281 lands. This ticket is natively blocked by it. Restoring early re-opens the exact PII exposure the reverts closed.
  • Re-verify the scrub against the data before re-adding the pages. #2281 landing is necessary, not sufficient — re-run the 2015-week sampling above (and at least one other decade) through /flowsheet/range and confirm the marker-row and show-heading counts have gone to zero. shows.legacy_dj_name and flowsheet.dj_name are two different exposure routes and the scrub must be confirmed on both.
  • website is a static export (output: 'export'), so these pages fetch api.wxyc.org from the browser. Public read CORS is already in prod via PUBLIC_READ_ORIGINS=https://wxyc.org,https://www.wxyc.org (feat(cors): allow wxyc.org origins on the public flowsheet read routes Backend-Service#2061/#2075) — credential-less, scoped to the public GET routes. Don't route this through FRONTEND_SOURCE; that grants credentialed access org-wide (the cors: '*' fallback combined with credentials:true when FRONTEND_SOURCE unset Backend-Service#1107 hazard).
  • The archive page takes the week as a query param (/playlists/archive?week=YYYY-MM-DD) — no SSR and no getStaticPaths over 2.6M rows.
  • Calendar days are not 24h and calendar weeks are not 168h. Derive end with calendar arithmetic, never start + 86400000. Verified live: 2026-03-08 is 23h, 2025-11-02 is 25h, and the fall-back week is 169h — which is why MAX_RANGE_MS is 8 days, not 7.
  • The repo cannot vitest a JSX-bearing .js file (esbuild reports "The JSX syntax extension is not currently enabled" despite vitest.config.js's esbuild.include). Any test-covered page or component must be named .jsx. Both restored pages already are.
  • Flowsheet history reaches back to at least 2004-11-01, not 2009 as some wxyc.org copy advertises.

Acceptance criteria

  • Scrub historical denormalized dj_name values to match the current resolution policy Backend-Service#2281 is closed and its scrub verified in prod.
  • Re-sampling historical weeks through /flowsheet/range finds no real-name-shaped values on marker rows or show headings.
  • pages/airplay-search.jsx and pages/playlists/archive.jsx are back on main, with their tests restored and passing.
  • content/page/programming.mdx links to both pages again; README.md sections restored.
  • The deliberate-gap note is removed from __tests__/wxycInfoLinks.test.js; the wxyc.info turndown guard still passes.
  • Live verification: https://wxyc.org/airplay-search and https://wxyc.org/playlists/archive both return 200 and render real data.
  • WXYC/wiki#93's "Accepted listener-surface gap" section is updated to record the gap as closed.

Notes for implementer

The legacy wxyc.info/radioWeek grid had defects found during the #215 build — it silently dropped shows on slot collisions (show 172677 was absent from the grid while radioShow?radioShowID=172677 returned a full playlist), could not render zero-height blocks, and clipped overnight shows at midnight, filing the after-midnight half under the next day. These are not parity targets. The successor page was built without them; don't reintroduce them chasing a faithful copy.

A week of range data costs 2,300–2,800 entries / 470–640 KB gzipped / 1.2–1.8s, which is why the page renders each show as a collapsed <details> rather than laying out ~2,500 rows.

website/CLAUDE.md is untracked in this repo — README.md is the committed documentation surface.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort:mTriage effort: multi-file featureenhancementNew feature or requestsev:medTriage severity: impact under common conditionstubafrenzyTubafrenzy decommissioning (org Project #36)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions