Skip to content

Latest commit

 

History

History
1366 lines (1242 loc) · 103 KB

av-stack-overview.md

File metadata and controls

1366 lines (1242 loc) · 103 KB

Devcon AV Stack - Map & Devcon 8 Readiness Assessment

Context

Ahead of Devcon 8 (Mumbai/India), we need a clear picture of everything AV-related in efdevcon/monorepo: livestreams, video delivery, graphic generation, and the data pipelines that feed the archive. The repo contains a handover document (docs/now.md, AV section pointing to pretalx-pipeline.md alongside this doc) which states the AV enrichment pipeline was never built because the AV team wasn't formed at the time. This document maps what actually exists, separates live from dormant, and lists what will break for DC8 if untouched.

For what happened during Devcon SEA's event week itself (livestream player built the day before, the monitor page, pipeline write-backs that erased YouTube ids and Swarm hashes, schedule publish cadence), see devcon-sea-event-week-retro.md.

Sections 1-9 are assessment only; §10 suggests sequencing and §11 records the architecture recommendations from the review discussion. Decisions belong with the AV team.

Assessment date: 2026-08-04. Items marked ✅ FIXED were implemented the same day - §12 is the changelog. On 2026-08-05 the §2c app decision landed (event-app) and its AV surface was built - §12c is that changelog. On 2026-08-10 compression + CDN cache headers landed on devcon-api (the §11.7 insurance) - §12e is that changelog. Reviewed 2026-08-10: findings re-verified against the repo and live API, GitHub links added, superseded statements corrected in place. Updated 2026-10-09, after the first Devcon 8 schedule release: §12g covers the Pretalx upgrade, the partner schedule sync, the release guard and the featured-speaker question. Line references may drift as code changes.


1. Architecture in one paragraph

There is no database and no media infrastructure of Devcon's own.

  • Hosting: the API (api.devcon.org) runs on Render. Committing to devcon-api/data/ redeploys it (Render boots the new instance in parallel, so no downtime), which is how git-committed changes reach the running API.
  • Data layer: devcon-api has no Prisma/SQLite/Postgres. Synced data lives as plain JSON committed into devcon-api/data/, loaded into memory at boot by devcon-api/src/data/store.ts. Between deploys the data is also updated on the fly: the Pretalx webhook swaps the in-memory store in real time (hooks.ts → store.ts), and PUT /sessions/sources/:id writes to memory immediately - the git commit is for the next boot (eventual consistency - AV commits are even tagged [skip deploy], sessions.ts, so they don't force a restart). Writes go back to git via devcon-api/src/services/github.ts (CommitSession). Git is the system of record.
  • Playback: no player library exists anywhere (no react-player/video.js/hls.js/plyr/ vidstack - verified against every package.json). No HLS, RTMP, DASH, WebRTC, Mux, or Cloudflare Stream. Everything is third-party iframes (YouTube, StreamEth, Etherna/Swarm, stm.live) plus one raw <video> in the archive's IPFS tab.
  • Ingestion: Pretalx (cfp.devcon.org) is the source of truth for schedule; YouTube is the system of record for video. AV enrichment happens via PUT /sessions/sources/:id, which updates memory and commits to git.
  • Consumers of the API: devcon-app (DC7) and event-app (the DC8 PWA - sessions/speakers/livestreams), the archive (archive/), devcon.org (/dips), devcon-ai (RAG embeddings built during sync), and Meerkat (session Q&A - pulls the schedule from Pretalx itself, the sync script pings it on every run so it refreshes; §3 #11).

Consequence: "building the AV pipeline" is entirely a data problem. No encoding, storage, or player work is implied.

2. The flow that exists today

High level (matches the internal architecture diagram, plus the consumers):

flowchart TD
    PX[Pretalx<br/>schedule published] -->|webhook| API[devcon-api]
    API -->|update in real time| STORE[in-memory store]
    API -->|trigger| GHA[GitHub Action]
    GHA --> ROS[generate run of show]
    ROS -.-> AV[AV]
    AV -->|enrich sessions with stream/video URL<br/>via git commit| DATA
    GHA -->|commit Pretalx data| DATA[(devcon-api/data<br/>- devcon monorepo)]
    DATA -.triggers api restart and is the source of truth at boot-up,<br/>giving us eventual consistency.-> API
    API -->|fetch sessions / speakers| APP[Devcon App / event-app]
    GHA -->|sync ping| MK[Meerkat]
    MK -.->|pulls schedule| PX
Loading

In detail:

Pretalx (cfp.devcon.org, schedule published)
  └─webhook──> POST /hooks/pretalx/:eventId/schedule   (devcon-api/src/controllers/hooks.ts)
                 ├─ in-memory atomic swap (store.ts, zero downtime)
                 └─ TriggerWorkflow() per WORKFLOW_MAP (hooks.ts)
                      ├─ sync-pretalx-<event>.yml ──> commits devcon-api/data/ ──> API restart
                      │                            ├─> devcon-ai `sync:sessions` (RAG embeddings)
                      │                            └─> POST app.meerkat.events sync ping (test-devcon-8, devcon8; §3 #11)
                      └─ run-of-show-<event>.yml  ──> upserts the AV team's run-of-show sheets (§2e)

AV / encoding vendor
  └─PUT /sessions/sources/:id (API key) ──> memory + CommitSession() to git
                                            fields: sources_{youtubeId,ipfsHash,swarmHash,
                                                    livepeerId,streamethId}, transcript_vtt,
                                                    transcript_text, duration

Details that matter:

  • WORKFLOW_MAP in hooks.ts wires devcon8's and test-devcon-8's run-of-show to regenerate on every schedule publish; devcon-7's does not (sync only).
  • The webhook also accepts slug-less POST /hooks/pretalx/schedule and resolves the event from the Pretalx slug in the payload. ✅ FIXED 2026-08 (§12): it used to fall back to devcon-7 for an unknown slug, silently resyncing the wrong event; unknown slugs now get a 400 + warning log.
  • The webhook acks 204 immediately and syncs detached (✅ 2026-08-19, §12f): the plugin sends the POST from inside Pretalx's release request, so holding the response for the sync stalled — and rolled back — orga-UI releases.
  • Both sync workflows also have a monthly cron fallback (0 23 30 * *), so data refreshes even if no schedule is published.
  • Releasing a Devcon 8 schedule is done by the speaker team in the Pretalx orga UI, nowhere else. pnpm pretalx:release (devcon-api, §12f) refuses devcon8 through a hardcoded NEVER_RELEASE list with no override (2026-10-08), so a script run cannot publish by accident. The first Devcon 8 schedule was released on 2026-10-09: the webhook dispatched the sync, which committed 316 sessions, 320 speakers and 11 rooms; the run-of-show workflow dispatched alongside it failed on the Google Sheets write quota, fixed the same day (§12g).
  • The devcon-7 sync additionally runs createPresentations() (Google Slides) and a glossary build - gated if (eventId === 'devcon-7') in sync-pretalx.ts. The slides pipeline end to end is in §2d.
  • Speaker emails never leave the sync: each speaker gets hash = HMAC-SHA256(EMAIL_SECRET, email) (pretalx.ts) so downstream tools can match a signed-in email against confirmed speakers without the API exposing addresses. Two implications: rotating EMAIL_SECRET changes every speaker hash, and if the env var is missing the sync just warns and skips hashing - no hard failure. No in-repo consumer today (the visa speaker form queries Pretalx directly), but treat the secret as stable.
  • AV write auth: PUT /sessions/:id and PUT /sessions/sources/:id check an API key against SERVER_CONFIG.API_KEYS (src/middleware/apikey.ts). ✅ IMPROVED 2026-08 (§12): the key is now accepted via the x-api-key header (preferred - query-string keys land in Render/proxy access logs); ?apiKey= still works for existing tooling. Issue DC8 vendor keys with the header form.
  • Pretalx instance: everything lives on https://cfp.devcon.org (migrated from mum.speakat.xyz in Aug 2026; speak.devcon.org retired before that). All PRETALX_BASE_URI entries in devcon-api/src/utils/config.ts now point there. Gotcha: a stale base URL is worse than a broken one - fetch strips the Authorization header on the cross-origin 301 redirect, so the sync runs anonymously and private events 401 while public ones keep working. The secret name PRETALX_API_KEY_MUMBAI predates the rename and is kept as-is.

2a. Field recap: everything the AV team writes, and how

Three write paths. Both endpoints authenticate with an API key (x-api-key header preferred, ?apiKey= query still accepted) and commit to git with [skip deploy].

Path A - PUT /sessions/sources/:id (the day-of enrichment endpoint, sessions.ts). ✅ FIXED 2026-08 (§12): now patch semantics - omitted fields keep their current value, an explicit '' clears (it used to be a full replace where a partial payload wiped everything unsent). Bumps the version of the session's own event (the devcon-7 hardcode of blocker #1 is gone).

Field Consumed by
sources_youtubeId YouTube embed in devcon-app + archive; yt.ts thumbnail/description push; coverage stats
sources_streamethId StreamEth iframe tab
sources_swarmHash Swarm player (devcon-app only; also gates the player-switcher pills, §7.3)
sources_ipfsHash Archive IPFS tab (dead gateway, §6) - vestigial since DC6
sources_livepeerId Nothing (1 session ever had one)
transcript_vtt Nothing - never parsed anywhere (§8)
transcript_text LLM/AI consumption only
duration Duration display in app + archive

Path B - PUT /sessions/:id (generic session update). True partial merge - only sent keys change, unknown keys are rejected. Can update any existing session field, notably resources_slides, title/description corrections, slot_* moves. ✅ FIXED 2026-08 (§12): it now bumps the event version too (it used not to, so corrections could serve stale to clients and OG cards indefinitely).

Path C - hand-edited git commit (no endpoint). Per-room livestream config in data/rooms/<event>/*.json: youtubeStreamUrl_1..4 (one per event day) and translationUrl. Survives Pretalx syncs only via spread order (§5.3).

2b. Automation inventory - every Pretalx/AV entry point

One table for everything that runs (or is supposed to run) without a human editing JSON by hand. "Manual" means someone runs a pnpm script locally.

Automation What it does Trigger Event scoping Status
Pretalx webhook (hooks.ts) Full resync into memory + dispatches workflows Pretalx "schedule published" plugin webhook Slug-resolved; unknown slugs rejected with 400 (✅ fixed, §12) Live
sync-pretalx{,-devcon8,-test-devcon-8}.yml Commits devcon-api/data/ to git (→ Render redeploy), builds devcon-ai RAG embeddings, pings Meerkat Webhook-dispatched + monthly cron (0 23 30 * *) devcon-7 / devcon8 / test-devcon-8 Live
run-of-show-{devcon8,test-devcon-8}.yml Upserts the AV team's run-of-show sheets, one per day and stage (§2e) Webhook-dispatched + manual dispatch devcon8, into the AV Drive next to the intro cards (since 2026-10-20); the test-devcon-8 job is switched off (if: false, run it locally); devcon-7 runs locally from committed data as the preview Live
pnpm sync:eventyay <event> (sync-eventyay.ts) Syncs a partner's eventyay schedule (Open Tech Summit) into our Pretalx event: proposals, WIP slots, tags, speakers (§12g) Manual; dry run by default, --apply writes test-devcon-8 / devcon8 (target table in the script) Manual
pnpm pretalx:release <event> (pretalx-release.ts) Releases a schedule version through the Pretalx API (orga-UI fallback, §12f) Manual devcon8 refused (NEVER_RELEASE); test-devcon-8 and devcon7-sea Manual
Meerkat ping (notifyClients() in sync-pretalx.ts) POST sync ping to app.meerkat.events, which then pulls the schedule from Pretalx Runs inside the sync script test-devcon-8 / devcon8 (MEERKAT_EVENTS map; ✅ 2026-10-09, §12g) Live
pnpm yt → syncThumbnails() (yt.ts) Renders devcon.org/api/social/av/:id at 1920×1080 and pushes via youtube.thumbnails.set() (105/run for quota) Manual, interactive browser OAuth sessions/devcon-7 hardcoded Manual
pnpm yt → syncDescriptions() Rewrites YouTube titles (truncated to fit "by " + a hardcoded | Devcon SEA suffix into 100 chars) and descriptions (from session description + tags), ledger youtube-descriptions.json Commented out in main() devcon-7 + SEA branding hardcoded Disabled
pnpm import:yt (import-yt.ts) Imports YouTube playlists (data/playlists.json) into session JSONs - how devconnect-arg's 418 sessions got in Manual, Google service account eventId = 'devconnect-arg' hardcoded Manual
pnpm stats:v (stats-video.ts) AV source-coverage report Manual ✅ Takes an event id since 2026-08 (§12) Manual
devcon.org social/OG routes On-demand edge-rendered cards with a Supabase render cache (see §4) HTTP, devcon.org/api/social/* DC7 placeholder art in dc8/ (asset swap pending, #6) Live (§12d)
generate-images.yml (nested .github) DC6 lower-thirds + social cards Hourly cron in a nested .github GitHub never executes DC6 Dead (§8)
pnpm slides (slides.ts) Google Slides PDF export + Drive migration (§2d) Manual data/slides/ gone → throws Dead (§9)

2c. The app question: devcon-app (DC7) vs event-app (DC8 PWA)

✅ DECIDED 2026-08-05: event-app carries DC8's AV surface (see §12c for what was built). The handover doc positions event-app as the new PWA going forward (offline-first: Dexie + SWR, Serwist service worker, optional native iOS/Android via Capacitor - see event-app/docs/architecture.md). devcon-app stays DC7-only; its blocker #3 is moot.

  • event-app's AV surface now exists (it originally had none - the zod models stripped the AV fields at validation). Today: SessionSchema carries sources_*, RoomSchema carries youtubeStreamUrl_1..4 / translationUrl (event-app/src/data/models/), the provider maps them (normalising the API's '' to undefined), and SessionMedia.tsx embeds recordings (YouTube, else StreamEth) with a live-stream fallback selected by event-relative day indexing (eventDayIndex/streamUrlForDay in components/schedule/utils.ts), plus a "Live translation available" link while a session is live/imminent and its room has a translationUrl.
  • Data source: the active provider is DevconApiProvider → devcon-api, with a runtime-switchable dataset (?dataset) offering test-devcon-8, devcon8 and devcon-7 (event-app/src/data/dataset.ts). A second, currently inactive provider (devcon.provider.ts) reads Pretalx directly through /api/pretalx - a server-side proxy hardcoded to EVENT_SLUG = 'test-devcon-8' (route.ts); flip that slug if the provider is ever reactivated.
  • Room screens (venue signage) live here now: /room-screens/[id] renders a per-room now/next display, relaid out 2026-09-17 for TVs and laptops (sized in a 16:9 design unit; branded header with the stage crest and clock, live progress bar, next-up rail). Its "Scan" row carries up to three QR codes: "Open in app" (the session), "Watch livestream" for the room's stream on the current conference day (anchored on now via the mockable clock, unlike SessionMedia which anchors on the session's own day - intentional; hidden when no stream URL exists), and "See questions" to Meerkat's presenter view for the room's stage, keyed by our room id since 2026-10-09 (only when Meerkat lists sessions for that stage, see docs/meerkat.md). /room-screens is the picker, one card per room with its live/next session. devcon-app has the DC7 predecessor at devcon-app/src/pages/room-screens/[id].tsx (text-only, no QR).
  • Meerkat's user-facing half is in event-app: the session pages show the questions (public API, SSE stream) and "Ask a question" links to GET /api/meerkat/go, which gates on the Supabase auth cookies + a paid Pretix ticket and 302s to Meerkat with a 5-min HS256 JWT (secret shared with Meerkat). For stage screens and streams, Meerkat's presenter view (/stage/<stage>) follows the live session; options and prerequisites in docs/meerkat.md. The schedule-sync half is §3 #11.
  • event-app's /api/admin/{datasets,search,inference} routes are the debugging UI for the devcon-ai RAG stack (per the handover doc).
  • devcon-app meanwhile shows a dismissable "Devcon 8 prep" banner but is otherwise still fully DC7-wired.

2d. Speaker slides pipeline (Google Slides, DC7)

How every DC7 speaker got a pre-made deck and how the archive got its PDFs. Built by Wesley between Oct and Dec 2024; everything lives in devcon-api plus one devcon.org page. Until 2026-09-28 nothing was written back to Pretalx, it only supplied titles, codes and speaker emails; since then the sync writes each deck's URL into a Pretalx question (step 2).

  1. Deck creation - clients/slides.ts CreatePresentationFromTemplate(title, pretalxCode, speakerEmails): the EF Google service account copies one template deck into a shared-Drive folder, names it <title> [<PRETALX_CODE>] and grants each speaker email writer access with the Drive notification email off (sendEmails = false). A name contains '[code]' lookup makes it idempotent. The first bulk run was manual (pnpm slides, 2024-10-11). The shared drive and the template deck are common to all events (SLIDES_DRIVE_ID, SLIDES_TEMPLATE_ID); the folder that receives the decks is per event, named after it (SLIDES_FOLDER_ID_TEST_DEVCON_8, SLIDES_FOLDER_ID_DEVCON8) and mapped per instance in utils/config.ts (2026-09-30), so test decks and Devcon 8 decks cannot share a folder. No defaults: unset means the run fails before touching Drive (2026-09-17, the DC7 ids were removed from the code); keep the ids out of docs. The DC7 template deck itself is shared "anyone with the link" (checked 2026-09-17); copies do not inherit that, but the master should be restricted so nobody outside can edit what every deck is copied from.
  2. Wired into the Pretalx sync - sync-pretalx.ts createPresentations() (2024-10-28): for every data/sessions/devcon-7/*.json without resources_presentation it creates the deck and writes https://docs.google.com/presentation/d/<id> into the JSON. runPermissions() (2024-11-09) re-grants writer access to the current speaker emails on existing decks, which covers speakers added later. Both run inside the Pretalx Sync GitHub Action with the Google credentials as secrets, so during DC7 every schedule publish also created decks for new talks. Since 2026-09-17 two gates replace the devcon-7 check: a hardcoded SLIDES_ALLOWED_EVENTS list in the sync (test-devcon-8 and, since 2026-09-30, devcon8; devcon-7 is over and stays out) and the SLIDES_EVENTS env opt-in for the run. CI sets none of the SLIDES_* variables, so no workflow creates decks; Devcon 8 decks are created by manual runs for now, the first one on 2026-09-30 for a single accepted talk (SLIDES_EVENTS=devcon8 SLIDES_ONLY_CODES=<code> pnpm sync:pretalx:devcon8), then on 2026-10-08 for the 43 Open Tech Summit sessions (§12g). SLIDES_ONLY_CODES limits a run to a few Pretalx codes and SLIDES_SKIP_PERMISSIONS=true creates decks without granting speakers, both meant for test runs. pnpm slides (§9) now exits immediately. A permissions pass (2026-09-18, reconcilePermissions() in the sync, rules in utils/slides-permissions.ts) runs after deck creation for the same events: current speakers get writer, the AV group from SLIDES_AV_GROUP gets reader, anyone grants are removed unless SLIDES_ALLOW_PUBLIC=true. A person's access is never revoked (since 2026-10-16): Drive does not record who granted a permission, so a speaker removed in Pretalx and a co-author shared by hand look the same. Writers who are not speakers are listed in the log ("left as is") for the team to check; addresses in SLIDES_KEEP_EMAILS are left out of that list, and inherited access is never touched. It only reports while SLIDES_SKIP_PERMISSIONS=true or SLIDES_PERMISSIONS_DRY_RUN=true. It reads decks by the id in the session JSON, so it needs the Google identity to see the decks: as of 2026-09-18 the external service account loses sight of files it creates seconds after creation (cause under investigation with the Workspace admins), which blocks this pass and the export. GOOGLE_IMPERSONATE_USER (domain-wide delegation, to be authorised by an admin) makes the pipeline act as an internal EF user instead. Partner tracks (2026-10-08): a deck whose session sits on a track listed in the instance's SLIDES_DELEGATE_TRACKS ([CLS] - OTS on both DC8 events) is shared with the partner's contact address from SLIDES_PLACEHOLDER_DELEGATE_<EVENT> only, at creation and in the permissions pass; the partner passes it on to its speakers. Outside those tracks a placeholder speaker address (§12g) also maps to that contact, real addresses are granted as usual. Deck link and no-Google-account speakers recorded in Pretalx (2026-09-28): the slides passes now run over every accepted or confirmed submission (accepted since 2026-09-29: the acceptance email may carry the link before the speaker confirms), scheduled or not, and write each deck URL into a submission question ("Slides deck", url, private, frozen so speakers see it read-only; id in PRETALX_QUESTIONS_SLIDES_DECK). Pretalx is the source of truth: the session mapper copies the answer into resources_presentation once the talk is published, and a URL that only exists on disk is backfilled into Pretalx. Drive refuses a silent grant to an address without a Google account and only accepts one with its invitation email (not delivered in testing, 2026-09-18); such addresses are recorded in a second question ("Slides: no Google account", text, private, frozen; id in PRETALX_QUESTIONS_SLIDES_NO_GOOGLE_ACCOUNT), shown read-only on the proposal page to organisers and to the speakers themselves, with a help text explaining the Drive invitation, and listed on the question's organiser page and in the API for the speaker team to share the deck by hand or to export to the run of show. Pretalx has no organiser-only mode for a question (an inactive one is hidden from the proposal page for everyone, limit_teams only scopes organiser teams), the deployed version ignores the per-question list filters, and its API rejected every tag update on a proposal (fixed by the 2026-10-08 upgrade, §12g), so speaker-visible read-only was the workable option (decided 2026-09-28). Addresses leave the list when they leave the talk or an organiser clears the field; Drive gives no signal for an accepted invitation. Non-public tags are now dropped from the session JSON by the session mapper (they used to be published like any other tag). Deck activity (2026-09-29): a third question, "Slides: last edit", holds untouched while Drive's last modifier of the deck is still the pipeline's own identity (the impersonated user, else the service account) and edited <date> afterwards, the DC7 event-eve nudge's signal made persistent for the speaker team and the run-of-show export. One files.get per deck per run, written to Pretalx only when the value changes, as fresh as the last sync; opening without editing counts as untouched, any content change by anyone counts as edited. Like the no-Google-account list it is mapped only on contact reads and never reaches the session JSON. Stale decks (2026-09-29): a talk that is rejected, withdrawn or otherwise no longer accepted/confirmed keeps its deck and the speakers' writer access, since the pipeline never deletes files or revokes access on its own. The sync lists such decks in the final problems list so the team can archive the deck or revoke access by hand. Field definitions, identical on test-devcon-8 (ids 178/179/180) and devcon8 (ids 181/182/183, created 2026-09-30 via the API; all: target submission, optional, not public, not visible to reviewers, active, freeze_after in the past):
    • "Slides deck", variant url. Help text: "[Read-only, set by the Devcon team] Your Google Slides deck for this session, shared with each speaker's email address. Build your presentation in it. If you cannot open or edit it, email speak@devcon.org and provide a Google account address." (plain text: the url variant's help text is not rendered as markdown on the deployed pretalx, the text variant's is.)
    • "Slides: no Google account", variant text, contains personal data. Help text: "[Read-only, set by the Devcon team] This email address has no Google account, so the deck could only be shared by invitation email. If you cannot edit the slides, contact speak@devcon.org and provide a Google account address."
    • "Slides: last edit", variant text. Help text: "[Read-only, set by the Devcon team] Untouched until someone edits the deck, then the date of the last edit, from the deck's Google Drive history." The mail template links https://devcon.org/presentation/devcon8/{proposal_code}/; pretalx templates have no placeholder for custom-question answers. This replaced the sync's own "we need a Google account" email (2026-09-18 to 09-28). Both writes need the event's write token, PRETALX_API_KEY_WRITE_TEST_DEVCON_8 or PRETALX_API_KEY_WRITE_DEVCON8 (one organiser token per event, each scoped to that event's team, mapped per instance in utils/config.ts since 2026-09-30); without it the slides passes do not run. pretalx:release uses the same mapping.
  3. Speaker-facing link - devcon/src/pages/presentation/[event]/[code].tsx (2026-09-28; the 2024 /sea/presentation/<code> page stays as is, so DC7 links keep working): devcon.org/presentation/{devcon8|sea|test}/<code>/ reads the submission's "Slides deck" answer live from Pretalx, server-side with a one-minute CDN cache, and redirects to it, so the link works before the schedule is published; only Google Docs/Drive URLs are followed and only accepted/confirmed talks resolve in production. Devcon SEA has no such question and falls back to api.devcon.org/sessions/<code>. Without a link the page shows "No presentation link found. Please contact the organisers." Speakers get the link from Pretalx mail templates.
  4. Event-eve nudge - RunPermissions (commit 553ca239a, 2024-11-12 00:59 Bangkok) reads the deck's lastModifyingUser. If the last editor is still the service account, meaning the speaker never opened the deck, it re-grants the speakers writer access, silently: despite its "Sending Notification Email" log line the grant has sendNotificationEmail: false, and the Drive-notification branch in the creation path was gated by a sendEmails flag that was false in every commit (checked 2026-09-18). Drive never emailed a DC7 speaker; the deck link travelled in Devcon's own emails (step 3), and a grant to an address without a Google account failed with "Grant manually" in the log.
  5. Post-event export to the archive - scripts/slides.ts (2024-12-09 to 12): exportSlides() pulled each deck as PDF through the Slides export URL into data/slides/devcon-7/<id>.pdf and set resources_slides to the API URL; cleanupSlides() deleted PDFs that were empty or byte-for-byte the untouched template (size heuristic); migrateSlides() uploaded the remaining PDFs to Drive, rewrote resources_slides to https://drive.google.com/file/d/<id>/view and removed the local files. That is why data/slides/ no longer exists and the script is dead today (§9). The archive still cannot show these links because of the slidesUrl field mismatch (§7.1).

When slides were visible during DC7 (from git; the slides client never held an anyone grant at any point in its history, so read access was never changed by code):

Date What became visible, and to whom
2024-10-15 Redirect route devcon.org/sea/presentation/<code> live, used by speakers
2024-10-28 Deck URLs committed to the public repo and served by the public API for all sessions; the decks themselves stayed restricted to speaker emails plus shared-drive members
12 to 15 Nov 2024 No change. Neither devcon-app nor the archive displayed deck or slide links to attendees
2024-12-09 to 12 PDF exports published as resources_slides, first API-hosted, then Drive links
Since then Still not rendered in the archive because of the slidesUrl mismatch (§7.1)

A stranger following the public URL hit Google's request-access screen before, during and after the talk. The only way a deck could be read early was a speaker changing its sharing themselves, which git cannot show. Slide content reached the public only in December, about four weeks after the event.

For DC8 the recommendation is to keep this pipeline, keep decks private until after the event as in 2024, and enforce that instead of leaving it to each deck's sharing setting: speakers write, the AV team reads from creation, the public reads only after the event (§11.9). Reuse checklist (allow-list, questions and manual runs done 2026-09-30; CI still pending): add SLIDES_EVENTS=devcon8 to the devcon8 sync workflow, with the Google secrets, the shared SLIDES_DRIVE_ID / SLIDES_TEMPLATE_ID and the per-event SLIDES_FOLDER_ID_DEVCON8 / PRETALX_API_KEY_WRITE_DEVCON8 as new repository secrets (the names carry the event, so the test workflow cannot receive them by mistake). The permissions pass and the /presentation/devcon8/ redirect exist already.

Test run on test-devcon-8 (local only; that workflow has no Google credentials and the event mirrors real Devcon 8 talks, so keep speakers out of it): in devcon-api/.env set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY (from the devcon-7 sync workflow secrets), SLIDES_EVENTS=test-devcon-8, SLIDES_DRIVE_ID=<AV shared drive>, SLIDES_FOLDER_ID_TEST_DEVCON_8=<test folder in it>, SLIDES_TEMPLATE_ID=<DC8 template>, PRETALX_API_KEY_WRITE_TEST_DEVCON_8=<write token scoped to the test event>, SLIDES_SKIP_PERMISSIONS=true, and optionally SLIDES_ONLY_CODES=<a few codes>. Then pnpm sync:pretalx:test; decks appear in the folder as <title> [<code>] and the URLs land in data/sessions/test-devcon-8/*.json, which should not be committed.

2e. Run of show (Google Sheets, DC8)

Rewritten 2026-09-18 with the AV team's own template. Before this the script deleted and rebuilt every tab on each Pretalx publish, wiping the crew's notes (old §5.1).

Shape. One Google Sheet per day and stage. devcon8 (AV_TREE_EVENTS) puts it in the AV team's Drive next to the intro cards, <AV folder>/$$EVENT/<Room>/<Day N - Mon D>/Run of Show · <Stage> · Day N, with the folder names shared with av-assets.ts (utils/av-drive.ts); other events use <ROS_FOLDER_ID>/Day N · ddd D MMM/<Stage name>, each copied once from the AV template (ROS_TEMPLATE_ID, the team's Google Sheet with its RUN OF SHOW and AV & TECH CUES tabs). The template's header row tells the script which layout it is: v1 is the AV team's original template, v2 the proposal built 2026-09-18 (ownership shown in header colours, ACTUAL START and Δ, REC from the Pretalx "do not record" flag, TRACK, checkboxes, a CUE TYPE link column and a cue tab linked by CUE REF), v3 the AV team's revision of v2 (2026-10-20, see below). Each generated sheet is recognised by its own header row (row 5 for v1/v2, row 2 for v3), so sheets keep working with the layout they were created from; layouts can live in one folder, which is how the side-by-side preview was made (ROS_ROOMS=main-stage with the v1 template, ROS_ROOMS=stage-1 with the v2 template). Layout definitions are in utils/run-of-show.ts, the format and clearing details per layout in LAYOUT_IO. Files are recognised by Drive appProperties (ros_event, ros_day, ros_room), so renaming a file or a Pretalx room is harmless. Each sheet also gets a CHANGES tab and a hidden _style tab (an untouched copy of the template used to re-apply row formats).

v3 (2026-10-20). Agreed with the AV team: a DAY / STAGE line, then headers on row 2. Columns # · STATUS · START · DUR · END · Q&A · MC · SESSION · SPEAKERS · SPEAKER CONTACT · FORMAT · SLIDES · DECK OK? · INTRO ANIMATION · STREAMING · REC · MODERATOR · MIC · NOTES · PRETALX ID. Dropped from v2: ACTUAL START, Δ, TRACK, ON SITE?, the cue sheet and the MORNING / AFTERNOON / END OF SHOW rows (the REMOVED section still appears when a session leaves). Rules that differ:

  • DUR is the Pretalx slot, Q&A included (the AV timer table: Talk 20 + 5, Lightning 5 + 2, Mixed 25 + 0, Workshop 50 + 0; the buffer is the gap to the next slot).
  • Q&A and SLIDES are fill-once: the sync writes them only while empty (Q&A from the timer table, other formats 0), then they are the crew's ("Laptop", a longer Q&A). A deck link that later changes in Pretalx is logged on CHANGES and highlighted, never applied.
  • MC is a formula: minutes from END to the next live row's START (red when negative).
  • SPEAKER CONTACT is synced: Telegram from Pretalx (speakers fetched with contacts, so it is only filled from the Pretalx source), else X, Farcaster or website, one line per speaker.
  • The stage's Q&A QR (devcon.org/qr/meerkat/<room id>, redirected by devcon /api/meerkat/ to the session a Meerkat moderator put live there, else the schedule's) is not a column: av-assets.ts saves it as $$EVENT/<Room>/Q&A QR - <Room>.png (one per stage, for offline use), and the sync links that file from the Digital Assets tab's "Q&A QR code" row.
  • STREAMING, INTRO ANIMATION are the crew's. The template's Digital Assets and Livestreaming Information tabs are the AV team's and never touched.

Contract with the AV team. The sync owns columns B..K (STATUS .. SLIDES / MEDIA) plus the key column PRETALX ID (R, appended to the template's 17). Columns L..Q (MIC CONFIG .. LAST EDITED BY) are the crew's and are carried over by Pretalx code:

  • a session moved within a sheet, to another stage or to another day takes its L..Q values with it (rows are indexed across every sheet before writing);
  • a session that leaves a sheet is parked at the bottom under "REMOVED FROM THIS RUN OF SHOW" as CANCELLED, notes intact; the CHANGES tab says where it went;
  • rows the crew adds by hand without a PRETALX ID (doors open, breaks) are kept and slotted by their START TIME;
  • a hand-set TBC, STANDBY or DELAYED survives a resync; MODERATOR and SLIDES / MEDIA are carried unless the sync has a value (the Google Slides deck URL from §2d lands in SLIDES / MEDIA);
  • every difference is appended to CHANGES (run, code, session, field, from, to) and the changed cells are highlighted until the next run;
  • # is a running COUNTA formula, END TIME is the template's start + duration formula (v2 adds Δ and the CUE TYPE link), STATUS and FORMAT keep the template dropdowns (FORMAT extended with the Pretalx types; v2 lists are strict), deck URLs render as a Slides link, rows auto-fit their wrapped text. Rows are rewritten in place below the header (never deleted), so the template's column-wide rules survive. All conditional colours are owned by the sync and re-created every run, because pasting row formats strips them (v1: STATUS; v2: STATUS, REC, Δ, "on stage now", "speaker not on site", banding). A new sheet gets the event time zone (the live rules use NOW()), and v2 gets the stage's stream link from the room data. AV & TECH CUES is emptied of the template's example content once, when the sheet is created, and never touched again. Dividers: MORNING before 13:00, then AFTERNOON, EVENING from 18:00.

Merge rules are pure and unit-tested in utils/run-of-show.ts; generate-run-of-show.ts does Drive/Sheets I/O only.

Running it. ROS_TEMPLATE_ID (the v3 template, also a repo secret) and the top folder are required (no defaults): AV_DRIVE_FOLDER_ID_DEVCON8 for devcon8 (the AV Drive, same secret as av-assets), ROS_FOLDER_ID for the other events; ROS_SOURCE=data reads devcon-api/data/ instead of Pretalx (ROS_DATA_DIR to point at a scratch copy), ROS_ROOMS limits rooms, ROS_DAYS limits days (1-based numbers, counted over the whole event), ROS_DRY_RUN=true plans without writing. ROS_ALLOWED_EVENTS in the script hard-limits which events may be generated; devcon8 was added 2026-10-20 and all its sheets were generated that day (38 sheets, 10 stages over 4 days). A local .env may still carry ROS_ROOMS from the devcon-7 preview: pass ROS_ROOMS= to lift it. pnpm run-of-show:devcon-7 is the preview from committed data; pnpm run-of-show:test runs test-devcon-8 from Pretalx. Google credentials: the shared GOOGLE_* pair (the devcon8 workflow too, the account that has the AV Drive); ROS_GOOGLE_* is only a fallback when GOOGLE_* is unset.

Drive gotchas found while building this. The service account is external to the EF Workspace. Files it uploads into an EF shared drive get the "EF Internal" classification within about half a minute and disappear from its view (not trashed, just no longer visible), which is what the "vanishing files" of §2d were. Files it copies and folders it creates are fine, so the template must be put in the folder by a person and the script only ever copies it. The account is a folder-level member, not a drive member, so the script walks the folder tree instead of searching the drive.

Open with the AV team. Whether stages without AV (breakouts, classrooms) need sheets (ROS_ROOMS), whether FORMAT should map Pretalx types onto their list instead of extending it, and who fills AV CUE REF on new rows (left blank today).

3. Devcon 8 blockers - independent hardcodes that fail silently

Ranked by severity. Each is a separate fix.

# Issue Location Effect
1 ✅ FIXED 2026-08 (§12) - updateEventVersion('devcon-7') hardcoded in the AV ingestion endpoint sessions.ts Every DC8 video PUT bumped DC7's cache-bust token; DC8 clients never saw new videos.
2 devcon8 rooms have no youtubeStreamUrl_* / translationUrl fields (re-verified 2026-08-10; the rooms were renamed in Pretalx and re-synced 2026-09-30, so the ids are now lotus-stage, lotus-1 to lotus-3, jasmine, lightning-stage, cls-stage, music-stage, workshop-2, workshop-3) devcon-api/data/rooms/devcon8/ All DC8 sessions render "No livestream available".
3 ✅ RESOLVED BY DECISION 2026-08-05 (§2c, §12c) - day→stream mapping hardcoded to Bangkok + Nov 12–15 2024 devcon-app/.../sessions/index.tsx DC8 ships on event-app, so this devcon-app hardcode is moot and stays as-is. event-app's replacement uses event-relative UTC-day indexing off event.startDate. Caveat: the math assumes startDate stays midnight-UTC form - changing it to local-midnight shifts every stream index by one.
4 ✅ FIXED 2026-08 (§12) - PRETALX_QUESTIONS_* IDs were unmapped for devcon8 / test-devcon-8 config.ts Speaker socials, expertise, audience, tags, keywords all silently empty.
5 ✅ FIXED 2026-08 (§12) - submission_type numeric IDs were hardcoded to DC7's pretalx.ts DC8 sessions fell through to the raw Pretalx type name, breaking type filters.
6 ◐ INFRA REPLACED 2026-08 (§12d), asset swap remaining - social cards were entirely DC7 Bangkok-branded devcon/public/social/dc8/ + devcon/src/pages/api/social/ The rendering stack was rebuilt on devcon.org with a Supabase cache; the DC7 art was copied to dc8/ as placeholders by explicit decision. Remaining for the rebrand: swap the dc8/ PNGs (same filenames, drop-in) and update the hardcoded "Bangkok, Thailand / 12-15 Nov 2024" block + Asia/Bangkok timezone in the two card templates.
7 ✅ FIXED 2026-08 (§12) - run-of-show rendered times in UTC generate-run-of-show.ts Wrong wall-clock for stage crew (IST is UTC+5:30).
8 ✅ FIXED 2026-08 (§12) - stats-video.ts was hardcoded to devcon-7 + Nov dates stats-video.ts The only AV coverage report couldn't run for DC8; now pnpm stats:v <eventId>.
9 yt.ts uses @google-cloud/local-auth (interactive browser OAuth) google.ts Cannot run in CI; YouTube push is manual-only. Nuance: a service-account path exists (AuthenticateServiceAccount, used by import-yt.ts) but service accounts can only read YouTube - writes (thumbnails/titles) need the channel owner's OAuth, so CI would require a stored refresh token.
10 ✅ FIXED + VERIFIED (2026-08-13) — end-to-end test now passes: PUT /sessions/sources/:id on test-devcon-8 returns 204 and the [skip deploy] commits land on main. Three bugs were found and fixed along the way: (1) SessionToJson assumed DB-era comma-string tags/keywords and 500'd on the file-store's arrays; (2) session ids are NOT unique across events (test-devcon-8 is a devcon-7 clone) and reads resolved via sessionMap (last event wins) while writes used array findIndex (first event wins) — an AV write briefly wiped a devcon-7 archive session's AV fields (restored same day); updateSession now resolves through the same map as getSession; (3) commits serialized the hydrated slot_room object into data files — now stripped. Verify with pnpm av:test-write (devcon-api) — it aborts if the test session resolves outside test-devcon-8. TOKEN MIGRATED OFF PERSONAL ACCOUNTS (2026-08-13): Render GITHUB_TOKEN is now a classic PAT on the devcon-website machine account (scope public_repo only; the account holds Write on the monorepo). Verified end-to-end: pnpm av:test-write returns 204/204, both [skip deploy] commits land on main, and the commits are committed by devcon-website — proof Render uses the machine credential, not a person's. The token can also read the Actions API (all 4 sync-pretalx workflows visible), so webhook-driven workflow_dispatch is covered. No individual's offboarding can break the AV pipeline any more. Also fixed while testing: two writes to the same session within seconds could 409 on a stale contents-API sha and surface as a 500 — CommitSession now re-reads the sha and retries (deployed, verified). REMAINING: PRETALX_API_KEY(_MUMBAI) repo secrets still need a confirmed owner; set a calendar reminder for the token's expiry. Recommended follow-up: regenerate test-devcon-8 with prefixed session ids so cross-event id ambiguity disappears entirely. github.ts (TriggerWorkflow and CommitSession), via GITHUB_TOKEN in the API's Render env (the workflow files themselves use the repo-scoped secrets.GITHUB_TOKEN, which is fine) Webhook→workflow triggering and AV session commits die when the account is deprovisioned. PRETALX_API_KEY(_MUMBAI) repo secrets need an owner too.
11 ✅ FIXED 2026-10-09 (§12g) - Meerkat schedule sync ping was gated to devcon-7 and hardcoded to the retired meerkat.events URL sync-pretalx.ts DC8 schedule publishes never notified Meerkat (session Q&A), so its session list went stale. The sync now pings Meerkat's per-event Pretalx sync endpoint for test-devcon-8 and devcon8 (see docs/meerkat.md).
12 ✅ FIXED 2026-08 (§12) - devcon8 event metadata was empty devcon-api/data/events/devcon8.json The API served a nameless, dateless DC8 event; now hand-authored (title, dates, venue). Event metadata has no sync - it stays hand-authored.

4. Social/OG cards and the YouTube thumbnail pipeline

Since §12d (2026-08-05), the cards render on devcon.org with a Supabase render cache. yt.ts fetches devcon.org/api/social/av/{id} at 1920×1080 and pushes it straight to youtube.thumbnails.set() - that route is the video thumbnail renderer, not just an OG endpoint. Idempotency ledger: youtube-thumbnails.json (581 IDs), throttled .slice(0, 105) per run for YT quota.

Current route inventory:

Route Size Consumed by
devcon.org/api/social/av/[id] 1920×1080 yt.ts → YouTube thumbnails
devcon.org/api/social/schedule/[id] 1200×630 devcon-app session share cards (?v= cache-bust); devcon.org Hero.tsx schedule card
devcon.org/api/social/schedule-u/[id] 1200×630 personal-schedule share cards
devcon-social.netlify.app/[name]/opengraph-image ticket the original DC7 attendee "social ticket" (legacy, still on the old social-ticket app)
devcon-social.netlify.app/av/[id]/opengraph-image 1920×1080 archive video OG (legacy, old app)

The devcon.org routes live in devcon/src/pages/api/social/ on the shared og-cache.ts service (details in §12d). They default to https://api.devcon.org - the old app's localhost:4000 API_URL default footgun is gone for these routes, but still applies to the legacy social-ticket app while it hosts the two remaining routes.

Cross-link with blocker #1: devcon-app cache-busts the schedule OG URL with ?v=${useEventVersion()}. Before the §12 fix, PUT /sessions/sources/:id bumped the wrong event's version, which also kept DC8 share cards pinned to stale renders - both sides of that are resolved.

yt.ts also contains a currently-disabled YouTube title + description rewriter (syncDescriptions(), commented out in main()): it retitles videos to fit <title> by <speaker> | Devcon SEA into YouTube's 100-char limit and regenerates descriptions from session data (ledger: youtube-descriptions.json, 578 IDs). If revived for DC8: the ' | Devcon SEA' suffix, the sessions/devcon-7 source, and the description boilerplate ("Devcon SEA was held in Bangkok… Nov 12 - Nov 15, 2024") are all hardcoded.

Historic duplicate: sessions.ts (GET /sessions/:id/image) launches Puppeteer to screenshot a Handlebars template, and has an unreachable 1920×1080 'video' branch (imageType hardcoded 'og') - someone intended it to do exactly what the av card route now does. puppeteer pinned at 18.2.1, DC6-era track names. Dead.

5. Destructive-behaviour risks (live footguns)

  1. ✅ FIXED 2026-09-18 (§2e) - Run-of-show destroyed the AV team's manual work. The old script unmerged and rewrote all formatting and rebuilt every tab on each run. It is now a keyed upsert: only columns B..K and the PRETALX ID key are written, the crew's columns L..Q travel with the session, and nothing is deleted silently. Residual risk: last write wins if someone is typing in L..Q at the second a publish fires, and hand-applied cell colours do not travel with a moved row (values do).
  2. Sync can delete committed data. sync-pretalx.ts unlinkSyncs any local room/session file whose id is absent from the Pretalx response. A partial API response deletes real data. Partly mitigated by concurrency: cancel-in-progress: false and pull --rebase --autostash -X theirs.
  3. Livestream config survives only by spread order. Room stream URLs persist across sync purely because of {...roomFs, ...room} in sync-pretalx.ts (Pretalx never returns those keys). Reversing that spread silently wipes livestream config.
  4. ✅ FIXED 2026-08 (§12) A schedule publish used to drop AV enrichment from the serving copy until the next redeploy - certain to bite during the event. replaceEventSessions now carries the enriched fields over (see §12).
  5. ✅ FIXED 2026-08 (§12) A partial enrichment PUT used to erase the rest - now patch semantics (§2a Path A).

6. Source coverage on disk

Counts re-verified 2026-08-10 (devcon8 and test-devcon-8 rows: 2026-10-09):

Event Sessions YouTube IPFS Swarm StreamEth Livepeer Transcript
devcon-0 … 6 1,079 ~all ~all ~all – – –
devcon-7 650 580 0 555 388 1 367
devconnect-arg 418 418 0 0 0 0 0
devcon8 316 0 0 0 0 0 0
test-devcon-8 11 0 0 0 0 0 0
  • devcon8 got its first synced sessions with the first schedule release on 2026-10-09 (316 sessions, 320 speakers, no AV sources yet). test-devcon-8 carries the seeded test sessions from §12c and a few mirrored DC8 talks; its Open Tech Summit mirror was removed the same day (§12g).
  • IPFS mirroring stopped after DC6, and the gateway in use (cloudflare-ipfs.com, archive/.../Video.tsx) is decommissioned - so the IPFS tab is broken even for the videos that have hashes.
  • Swarm is the surviving decentralized mirror (555/650 for DC7), but the archive's Swarm player tab is commented out - only reachable in devcon-app.
  • Livepeer is 1 session. STREAMING_URL = 'https://live.devcon.org/' still exported from devcon/src/utils/constants.ts and devcon-app/src/utils/constants.ts with zero consumers.

7. Confirmed bugs (independent of DC8)

  1. Slides never render for any session. archive/.../Video.tsx reads video.slidesUrl; the API serves resources_slides. slidesUrl appears in zero session JSON files. 527 DC7 + 287 DC6 sessions have slides that are invisible. archive/src/types/index.ts is entirely Gatsby-era naming.
  2. Related videos always empty. store.ts intentionally skips vectorization with the comment "no client uses it" - but archive/src/services/devcon.ts does call /sessions/:id/related. The comment is wrong; the archive silently gets nothing (live: 404, re-verified 2026-08-10). The working replacement is devcon-ai/src/routes/recommend.ts.
  3. Player switcher hidden. devcon-app/.../sessions/index.tsx gates the source pills on sources_swarmHash, so YouTube+StreamEth sessions offer no choice.
  4. New events invisible in archive search until archive/src/hooks/useArchiveSearch.ts is hand-edited (hardcoded event list).
  5. Archive playlists are dead code. archive/src/services/playlists.ts
    • 24 JSON files are never imported by any route, and use the old Gatsby path scheme. Video.tsx renders a playlists prop the page never passes.
  6. ✅ FIXED 2026-08 (§12b) Room ids collide across events in the store. roomMap was keyed by bare room id, but ids repeat across event folders, so whichever event's folder loaded last silently overwrote the others - live on production at the time (DC7 main-stage sessions served another event's room and lost their youtubeStreamUrl_* config). Fixed by stamping rooms with their event folder and namespacing the map key to eventId/roomId.
  7. Session ids have the same latent collision - sessionMap is keyed by bare slug, and 5 duplicates already exist (test-devcon-8 mirrors 4 DC7 talks; securing-ethereum exists in devcon-1 and devcon-7), last-loaded wins. Harder to fix than rooms because GET /sessions/:id is a public bare-id contract (archive/app deep links). Not fixed; needs a decision (e.g. prefer the newest event on conflict, or event-scoped lookups). Since 2026-10-08 the sync at least reports it: a same-event collision (two talks with one title) and a cross-event one land in the run's problems list. The first Devcon 8 sync (2026-10-09) listed 43 ids that also exist in test-devcon-8, the Open Tech Summit sessions mirrored there (§12g); a bare-id lookup for those resolved to whichever event loaded last. Cleared the same day: the 43 test proposals were deleted and test release 0.36 synced them out.
  8. Production /events publicly serves three phantom events (re-verified 2026-08-10): 0 (stray data/events/0.json, an artifact of import-yt.ts ensureEventFile()), devcon-mumbai-playground (test instance, removed from PRETALX_INSTANCES but its data files remain), and test-devcon-8. initStore loads every file in data/events/ - there is no draft/hidden flag. The archive is only shielded by its own hardcoded event list (§7.4).

8. Buried assets nobody reads

  • 1,092 whisper.cpp transcripts in devcon-api/data/transcripts/ devcon-{0..6}/ (ggml-base.en, full timestamp arrays). Zero readers - no import, route, or build step touches that directory.
  • ~280 MB of DC7 per-room live-caption CSVs in data/transcripts/devcon-7/ - transcriptions-<room>.csv, one per stage, with Recognition plus 10 translation columns (bn-IN, fil-PH, hi-IN, id-ID, km-KH, ms-MY, my-MM, th-TH, vi-VN, zh-CN). Raw AV-booth output. Nothing joins them back to sessions by slot_start/slot_end. breakout-1/breakout-2 are header-only (nothing captured).
  • transcript_vtt is never parsed. No .vtt parser, no <track> element, no caption rendering anywhere in the monorepo. Some values are the literal string "No VTT link provided". Only transcript_text is used, and only for LLM consumption.
  • Transcripts are not in the current RAG index. devcon-ai/scripts/sync-sessions.ts builds embeddings from title/speakers/tags/description only; it does carry youtube_id in metadata. The DC7 transcript corpus is reachable only via the older devcon-api OpenAI Assistants vector store.
  • devcon-api/generated/ - 1,466 committed files (_youtube.txt ×413, _1080.png ×517, _social.png ×519) of DC6 lower-thirds and social cards, produced by generate-images.yml (hourly cron, Node 14) running yarn scripts:generator - a script that exists in no package.json, in a nested .github GitHub never executes. Fully broken; artifacts stale.
  • devcon-api/data/audio/devcon-6/ - 7 mp3s (DC6 opening ceremonies) powering GET /rss/podcast. Abandoned experiment.
  • devcon-api/data/edge-cases.json - hand-maintained notes on YouTube playlists that import:yt couldn't ingest (channel pages instead of playlists, etc.). Zero code readers; useful context if the devconnect-arg import is ever re-run.

9. Dormant / dead inventory (safe-to-delete candidates)

  • devcon-api/src/scripts/encrypt.ts - not in package.json, data/accounts/ gone.
  • devcon-api/src/services/email.ts - nodemailer SMTP sender with three templates (incl. accreditation-confirmation); zero importers in src/. Leftover from the removed account system.
  • devcon-api/src/utils/ {profile,zupass,web3}.ts - zero importers each; more account-system remnants (utils/account.ts by contrast is live, used by the Pretalx and DIPs clients).
  • devcon-api/src/scripts/slides.ts - data/slides/ doesn't exist, so migrateSlides() throws; exportSlides() commented out.
  • devcon-api/src/clients/recommendation.ts
    • data/vectors/ - deliberately disabled.
  • devcon-api/src/types/schedule.ts - Prisma-shaped {connect:{id}} payloads, fossilized; live replacement is pretalxToStoreData() in hooks.ts.
  • devcon-api/src/services/at-slurper/main.ts - hardcoded mock events + Notion DB id, and it runs on API boot (imported for side effects in controllers/at-slurper.ts).
  • atproto-slurper/slurper/server.ts - firehose call commented out in both branches; hardcoded cursor override in millis where Jetstream expects microseconds; backfillData() fully commented out. Only the HTTP endpoints are live.
  • devcon-ai/src/routes/generate-image.ts
    • base-model.ts - readFileSync reference PNGs (mumbai-character.png, base-model.png, dc8-bg.png) that don't exist → ENOENT on any request. generate-image.ts is the sole consumer of @imgly/background-removal-node.
  • devcon-ai/src/lib/replicate.ts - zero importers, AVATAR_PROVIDER referenced nowhere, replicate not in package.json.
  • Workflows (/.github/workflows/): run-of-show-devcon-mumbai-playground.yml and sync-pretalx-devcon-mumbai-playground.yml (scripts don't exist; instance removed from PRETALX_INSTANCES), ai-content-prep.yml (on: push commented out, yarn in a pnpm repo), devcon-db-cleanup.yml (no DB any more). (devcon-archive.yml, listed here previously, has since been deleted.)
  • devcon-app/src/pages/streams.tsx - ops stream wall, gated on youtubeStreamUrl_4 (DC7 day 4).
  • devconnect-app stages pages - replaced by stubs linking to devconnect.org/#videos; real code sits in page_backup.tsx with ~20 commented-out YouTube URLs from live hand-editing.

10. Suggested sequencing (for discussion with the AV team)

Before DC8 content exists - unblock the pipeline:

  1. ✅ DONE 2026-08 (§12) - the 'devcon-7' hardcode and the webhook memory-merge gap (§5.4) are fixed; the enrichment path is solid (§11.5).
  2. ✅ DONE 2026-08 (§12) - PRETALX_QUESTIONS_* and submission_type IDs mapped for devcon8 and test-devcon-8 (#4, #5).
  3. ✅ DONE 2026-08-13 - GITHUB_TOKEN (Render) migrated to the devcon-website machine account and the AV write path verified end-to-end (#10). Still to do: reconfirm ownership of the PRETALX_API_KEY(_MUMBAI) secrets against cfp.devcon.org.
  4. ✅ DECIDED 2026-09-18 (§2e) - run-of-show mutability: the sync owns columns B..K, the AV team owns L..Q, keyed by Pretalx code. ✅ 2026-10-20: devcon8 is in ROS_ALLOWED_EVENTS with the AV team's v3 layout, sheets generated in the AV Drive; the AV team reviews it on real data and the layout gets one round of changes.
  5. ✅ DONE 2026-10-09 (§12g) - Meerkat's per-event sync endpoint is pinged for test-devcon-8 and devcon8 (#11).

Before DC8 doors open - livestream surface: 6. ✅ DONE 2026-08-05 (§12c) - event-app carries DC8's AV surface (§2c). Schemas extended, livestream/recording embed + translation link + room-screen stream QR built and verified headlessly. devcon-app's #3 hardcode left as-is (moot). 7. Event metadata: ✅ DONE 2026-08 (§12) - devcon8.json authored (#12). Room stream fields (#2) still pending (need the DC8 YouTube channels). 8. ◐ HALF DONE 2026-08 (§12d) - social-ticket replaced by cached devcon.org routes; remaining: swap the dc8/ placeholder art + the hardcoded DC7 location/date/timezone in the card templates (#6). 9. ✅ DONE 2026-08 (§12) - run-of-show UTC → event-local time (#7). 10. ✅ DONE 2026-08 (§12) - stats-video.ts takes an event id (#8).

Independent of DC8 - cheap credibility wins: 11. slidesUrl → resources_slides (§7.1). One-line fix, unlocks 814 sessions' slides. 12. Either build the /related vectors or point the archive at devcon-ai/recommend (§7.2). 13. Swap the dead cloudflare-ipfs.com gateway (§6).

11. Architecture recommendations (from the review discussion)

Guiding constraint: during the event the infra must not go down, no matter what - Pretalx being slow or venue internet dropping are expected, not exceptional.

  1. Keep the serving architecture as is. Its accidental genius is that nothing at runtime depends on Pretalx: the API serves from memory, boots from git, and only talks to Pretalx on a schedule publish. If Pretalx is slow or down mid-event, nothing user-facing degrades - updates just wait. A database or a live-Pretalx dependency would both be steps backward on availability. Client-side resilience is covered by event-app's offline-first design (Dexie + SWR).

  2. "Pretalx as source of truth" - adopt as an editing surface, not a serving path. Pretalx custom fields are fine for anything decided before the event (room stream URLs, translation URLs), ingested at sync time - that would also kill the hand-edited room-JSON overlays (#2). But never let a consumer read Pretalx at runtime (this includes not activating event-app's dormant direct-Pretalx provider for the event).

  3. Keep PUT /sessions/sources/:id for day-of enrichment. It updates memory instantly, commits to git for the next boot, doesn't touch Pretalx, triggers no resync, and can't collide with the run-of-show sheet. Moving day-of enrichment into Pretalx doesn't fit its update model: the webhook only fires on schedule publish, so real-time updates would mean constant re-publishes (each one a full resync that also destroys the AV team's run-of-show edits, §5.1) or a new answers-poller - more infra, not less.

  4. Rejected: dual-writing enrichment to Pretalx custom fields. Technically possible without a resync (POST/PATCH /api/events/{event}/answers/), but: it puts Pretalx in the enrichment hot path (fail the PUT, or build fire-and-forget retry machinery); two writable copies drift, and the sync's {...fsSession, ...session} precedence means a stale Pretalx answer would clobber a newer git correction on every publish; and it needs an organizer-scoped token in the API's env - a much bigger blast radius than the current narrow key. Git history already provides the durability argument.

  5. The two code fixes that matter for event days: the 'devcon-7' version-bump hardcode in the enrichment endpoint (#1) and the memory-merge gap (§5.4). Both are small; together they make the enrichment path genuinely solid. ✅ DONE 2026-08 (§12), plus patch semantics on the enrichment PUT (§5.5).

  6. Social/OG images: keep on-demand rendering. The cards are pure derived artifacts (title + speakers + track art), regenerated automatically on change via the ?v= cache-bust; storing them upstream in Pretalx would only create a stale second copy, and Pretalx has no per-session image slot anything reads. Their failure mode is crawler-facing and cosmetic, not event-critical. If belt-and-braces is wanted later, the right "upstream" is pre-rendering to static files at sync time (the resurrected DC6 pattern, §8) - after the real blockers. ✅ Implemented 2026-08 (§12d) as the Supabase-cached variant on devcon.org: still on-demand, but render-once with stale-serving, deploy-proof storage, and no localhost default.

  7. Cheap availability insurance: Render is the only real single point of failure - consider CDN caching on the hot GET endpoints for event days so even an API restart mid-keynote is invisible. ✅ IMPLEMENTED 2026-08-10 (§12e) on the origin side; ✅ ACTIVATED 2026-08-19 via Render Edge Caching (see §12e update) — hot GET routes now serve cf-cache-status: HIT from the edge.

  8. Repo weight is a deploy-time tax on everything (found 2026-08-17): a SHALLOW clone of the monorepo is ~1.2 GB - devcon-api/data/ alone is ~650 MB (Devcon 6 ceremony MP3s under data/audio/, ~200 MB of DC7 transcript CSVs under data/transcripts/) and devcon/public/ ~540 MB (incl. a 17 MB presskit PDF). Every Render build and every Netlify build across all projects re-downloads this on every commit. When Render's clone/cache throughput degraded on 2026-08-14 (their side - phase went 1m43s → 10-27 min with identical repo content; support ticket territory), deploys jumped from ~5 min to 13-28 min, and the repo size is the amplifier. Recommendation: move data/audio/ + data/transcripts/ to object storage (they're read-rarely blobs, not schedule data) and prune devcon/public/

    • halves every clone in the org. Not urgent for DC8, but do it before the repo grows another gig of DC8 media.
  9. Slides for DC8: keep the per-talk Google decks, keep them private until after the event, and enforce it (§2d). Keep the decks because the AV team needs every deck URL in advance to pre-load and cue, a deck that exists from day one is why 527 of ~650 DC7 sessions ended with slides, and the archive fills itself. Keep them private because that is what DC7 did in practice (§2d timeline: slide content reached the public only in December) and it removes the pre-talk leak question entirely. What was wrong last year is that privacy relied on nobody touching a deck's sharing setting, and grants only ever accumulated. The URL being public everywhere (API, repo, one swap from the social link) is fine as long as read access is what the pipeline controls. Desired state, enforced by a scheduled permissions pass (an extension of runPermissions() plus permissions.list), run daily during the event and on every sync:

    • Speakers: writer from creation, as today. Revoke when a speaker is removed from the talk in Pretalx.
    • AV team: reader from creation, through one Google Group (or shared-drive membership), so they hold URL and content ahead of everyone else.
    • Public: nothing until the event is over. Any anyone grant found on a deck during the event is removed, whoever added it. A speaker can re-share their deck, and writersCanShare is not available for shared-drive files, so reconciliation is the only control.
    • Never anyone writer, before or after the event. Strip it whenever found.
    • Publish once, after the event: a single scheduled run exports every deck to PDF into resources_slides with the existing export code, so later speaker edits do not change the on-record version, and optionally adds type: anyone, role: reader, allowFileDiscovery: false on the decks themselves. This replaces the manual December 2024 pass. Prerequisites: confirm the Workspace or shared-drive policy allows "anyone with the link" sharing if the decks themselves are to be opened after the event (per-user external grants already work, link sharing is a separate setting); lift the devcon8 is in SLIDES_ALLOWED_EVENTS since 2026-09-30; each run still opts in with SLIDES_EVENTS=devcon8 and needs SLIDES_FOLDER_ID_DEVCON8 and PRETALX_API_KEY_WRITE_DEVCON8 (a token scoped to the devcon8 team, verified 2026-09-30: it can create questions and answers on devcon8 and nothing on the test event). The three slides questions exist on devcon8 since 2026-09-30 and their ids are in the devcon-api config and the /presentation/ redirect page, so devcon.org/presentation/devcon8/<code>/ resolves as soon as a deck link is written. Alternative if stage AV does not project from the decks and the Drive infra should go: a private Pretalx file question (not the native Resources feature, which is public on the talk page as soon as the schedule is), released by the sync into resources_slides after the event, since anything in a session JSON is public on landing and the Pretalx file URL is unauthenticated once known. Either way the archive needs the slidesUrl fix (§7.1) or nothing renders.

12. Changelog: fixes implemented 2026-08-04

All in devcon-api/; every behavioral change carries a test that failed before the fix (TDD), full suite 19 tests green, tsc --noEmit clean. Sections above are marked ✅ FIXED where they describe pre-fix behavior.

Enrichment endpoint (src/controllers/sessions.ts)

  • Version bump uses the session's own event (was hardcoded 'devcon-7', blocker #1). Why: every DC8 video PUT bumped DC7's cache token, so DC8 clients and the ?v= OG-card cache-bust would never see new videos.
  • Patch semantics on PUT /sessions/sources/:id (body.x ?? data.x ?? fallback, was body.x ?? '' - §5.5). Why: a vendor tool sending only a YouTube ID silently wiped the session's swarm hash, transcripts and duration. Omitted = keep, explicit '' = clear; documented in the swagger comment.
  • Generic PUT /sessions/:id bumps the event version too. Why: title/slides corrections updated memory but never told clients to refetch.
  • Enrichment survives the webhook resync in memory (§5.4): replaceEventSessions now carries ENRICHED_SESSION_FIELDS (sources_*, transcripts, duration) over from the previous in-memory copy; Pretalx still wins for its own fields. Why: a schedule publish dropped all videos from the serving copy until the next redeploy - certain to bite during the event when publishes and enrichment overlap.
  • Unknown Pretalx slugs are rejected (400 + warning; the || 'devcon-7' fallback is gone). Why: a webhook from a new/renamed Pretalx event silently resynced devcon-7. The test mocks the Pretalx client and asserts the sync is never invoked.
  • x-api-key header accepted alongside ?apiKey=. Why: query-string keys land in Render/proxy access logs; DC8 vendor keys should travel in the header.
  • PRETALX_QUESTIONS_* mapped for devcon8 + test-devcon-8 (blocker #4), ids fetched live from cfp.devcon.org. Deliberate omissions are commented in the config: DC8 merged Website+Github into one question (mapped to WEBSITE), has Bluesky (170) instead of Lens, and no Keywords question; the test event also lacks Farcaster and Tags. Why: without these, speaker socials/expertise/audience/tags sync silently empty.
  • Featured speakers: an organizer writes #featured in the speaker's Internal notes in Pretalx (organizer-only, never shown to speakers). Until 2026-10-21 this was a "Featured speaker" speaker question, but Pretalx shows every active speaker question in the speaker's own profile and speakers ticked it themselves, so it was deactivated on devcon8 (#186) and its config removed. The sync collects the marked speakers into the event record's featuredSpeakers (speaker files are shared across events, so the flag cannot live there), rewriting the list whenever the notes are readable, so it can shrink to empty. event-app lists them first and, while the list is empty, falls back to speakers with a featured session (Pretalx's own is_featured on the proposal, mapped as featured, a separate organizer-only flag).
  • mapSubmissionType extended with DC8 + test-devcon-8 ids (blocker #5). Talk/Keynote → Talk, workshops (1h/1h30/2h) → Workshop, Mixed Formats → Panel, Lightning Talk → Lightning Talk. "Experience" (89/100) intentionally unmapped - a genuinely new format, better shown as itself than mislabeled. Verified end-to-end with a real sync of test-devcon-8 (types and expertise populate).
  • devcon8.json metadata authored (blocker #12): Devcon 8, edition 8, 3-6 Nov 2026, Mumbai, Jio World Centre (+ address/website/directions), key-parity with devcon-7 verified. Values sourced from devcon.org's own copy. Why: nothing fills this file automatically; the API served a nameless, dateless DC8 event.
  • Run-of-show renders event-local time via EVENT_TIMEZONES (devcon8 → Asia/Kolkata); main() guarded by require.main === module so the file is importable by tests (blocker #7). Why: the sheet is wall-clock for the stage crew and IST is UTC+5:30 - an offset UTC formatting can never produce.
  • stats-video.ts takes an event id (pnpm stats:v devcon8) and derives days from the event file's start/end dates (blocker #8). devcon-7 output verified identical to the old script (51/107/126/52 sessions per day).

Test infrastructure

  • Four new test files (sessions.test.ts x6 tests, hooks.test.ts x2, store.test.ts x1, generate-run-of-show.test.ts x2, since folded into utils/run-of-show.test.ts, §2e); GitHub and Pretalx clients mocked, nothing touches the network.
  • package.json: jest moduleNameMapper for the @/ / @lib/ aliases (mirrors tsconfig 1:1) - without it no test importing app can run at all.

§12b. Second round, same day

Building the event-app AV slice surfaced a production bug; both the fix and the slice landed together:

  • devcon-api: room-collision fix (§7.6). GetRoomData() stamps each room with its event folder; roomMap keys are now eventId/roomId, and every resolution site (boot, event rooms, updateSession, createSession, webhook resync) passes the session's event. Found because the new event-app livestream logic read slot_room.youtubeStreamUrl_2 from the live API and got a playground room instead of DC7's main-stage. Regression test in store.test.ts; API responses now also carry eventId on room objects (additive). The session-id twin of this bug (§7.7) is documented but not fixed.
  • event-app: first AV surface (the "schema + minimal player" slice). SessionSchema gained sources_{youtubeId,streamethId,swarmHash}, RoomSchema gained youtubeStreamUrl_1..4 + translationUrl (the zod models used to strip these at validation), the provider passes them through, and a new EventSchema/getEvent()/useEvent() exposes event start/end dates. New SessionMedia component on the session page: recording embed (YouTube, else StreamEth) when sources exist; otherwise the room's stream for the session's conference day (derived from the event startDate - no day-of-month hardcodes - via the mockable useNow clock) when the session is live or starting within the hour; otherwise nothing. Verified headless against devcon-7 data: recording renders, mocked-live session picks the correct day-2 main-stage stream, idle session renders nothing.

12c. Changelog: event-app AV surface, 2026-08-05

The §2c decision was made (event-app) and the surface completed the next day. All changes verified by per-task review plus a whole-change review, and headlessly with playwright (API stubs + ?mockNow): live session embeds the day's stream and shows the translation link, a recording wins over the stream, the room screen renders the livestream QR. tsc --noEmit clean.

  • Shared day→stream helpers: eventDayIndex(tMs, eventStartIso) and streamUrlForDay(room, tMs, eventStartIso) extracted from SessionMedia into event-app/src/components/schedule/utils.ts (UTC-calendar-day indexing, 1-based, bounded by STREAM_FIELDS).
  • Room screens (RoomScreen.tsx): second QR ("Watch livestream") for the current day's room stream, same QR options as the session QR, anchored on now; hidden when no URL, no crash offline.
  • Translation: SessionMedia renders a "Live translation available" link (plain anchor, new tab) while a session is live or starts within the hour and its room has translationUrl - with or without an embed.
  • Timezone validation: the UTC-day math was traced against DC7's .add(7,'hours') convention - no off-by-one for IST session hours, provided startDate stays midnight-UTC form (see the #3 caveat).
  • Test data: data/rooms/test-devcon-8/ {keynote-stage,stage-1}.json seeded with the real DC7 livestream VODs, one distinct video per day slot (keynote-stage carries DC7 main-stage days 1-4, stage-1 carries DC7 breakout-1 days 1-4; all eight verified alive via YouTube oEmbed, www.youtube.com/embed/... form), and DC7's real translation portal (https://stm.live/Mainstage, still live) as translationUrl; data/events/test-devcon-8.json gained startDate/endDate (copied from devcon8) - without them eventDayIndex returns null and the seeded config is unreachable (caught by the final review; the verification stub had masked it). ?dataset=test-devcon-8 is now testable end to end.
  • Known accepted residual: the pre-existing bare-slug session collision (§7) means two DC7-mirrored slugs in test-devcon-8 can surface the seeded URLs if deep-linked during their November windows - root cause unchanged, deliberately not fixed here.

12d. Changelog: social cards moved to devcon.org with a Supabase render cache, 2026-08-05

Decision context: §11.6 recommended keeping on-demand rendering; the requirement added was a strong cache that survives rebuilds. Implementation generalizes the ENS ticket route's proven pattern (render → jpeg → public Supabase bucket, 12h Last-Modified staleness, serve-stale-on-failure).

  • New shared service devcon/src/services/og-cache.ts: serveCachedImage() encodes the contract - bucket-first read, render+upsert on miss/stale, serve the stale copy when the render or devcon-api fails, x-og-cache: hit|render|stale header, CDN s-maxage=43200, stale-while-revalidate=86400. Buckets auto-create (fixed en route: Supabase signals a missing bucket as HTTP 400 with "Bucket not found" in the body, not an outer 404). The existing api/ticket/[...slug].tsx now imports from this service, behavior unchanged.
  • Three new routes (Pages API, node runtime, satori templates lifted verbatim from social-ticket, in devcon/src/pages/api/social/): devcon.org/api/social/schedule/[id] (1200×630), /api/social/av/[id] (1920×1080 q85, the YouTube thumbnail source), /api/social/schedule-u/[id] (personal schedule). Fonts and art preloaded from devcon/public/{fonts,social}/ as data URLs - no runtime asset fetches. Speaker avatars prefetch with a blockie fallback so a dead avatar host cannot fail a card.
  • Consumers flipped: devcon-app session + personal-schedule share URLs (keeping ?v= as the crawler cache-buster), devcon.org Hero.tsx schedule card, and yt.ts thumbnails. The /[name] attendee card and the archive's OG remain on the old devcon-social.netlify.app app for now.
  • The cache of record is the social-cards Supabase bucket - external to Netlify, so deploys/rebuilds never evict it. Verified live: render→hit with byte-identical serves; cards publicly reachable at the bucket URL; with devcon-api fully dead, a previously-rendered card still serves 200 (uncached ones fail contained with 503).
  • Data-helper fix caught during the port: GET /account/:id/schedule wraps its payload as {user}, not {data}.
  • No new env vars: SUPABASE_* already on Netlify; DEVCON_API_URL optional (defaults to https://api.devcon.org - the old app's localhost:4000 default footgun is gone).
  • Remaining for #6: the actual DC8 rebrand - swap devcon/public/social/dc8/ PNGs (drop-in, same filenames) and update the hardcoded DC7 location/date copy and Asia/Bangkok times in the schedule/av templates.

12e. Changelog: compression + CDN cache headers on devcon-api, 2026-08-10

The API served everything uncompressed (the full devcon-7 session list was 23.4 MB per request, 1.6 MB gzipped) and, despite being proxied by Cloudflare, nothing was edge-cached (cf-cache-status: DYNAMIC - no JSON endpoint sent Cache-Control). This implements the §11.7 insurance: cached GETs keep serving through an API restart. All in devcon-api/, tsc --noEmit clean, headers verified against a local boot:

  • compression middleware in src/app.ts (23.46 MB → 1.63 MB on the session list).
  • New src/middleware/cache.ts - publicCache(maxAge) sets Cache-Control: public, max-age=N, s-maxage=N, stale-while-revalidate=2N at header-write time, GETs with <400 status only (webhooks, PUTs, ai and error responses stay uncacheable); handlers that set their own Cache-Control win. Cached routes get Access-Control-Allow-Origin: * because Cloudflare ignores Vary: Origin; safe, all catalog consumers fetch non-credentialed.
  • TTLs: 60s on the hot catalog routes (sessions/speakers/events/rooms/version), 3600s on /dips* + /rss/podcast, /static 1 day, /data 1 hour. 60s keeps day-of updates (schedule publish, AV enrichment PUT) propagating within ~a minute; edge + browser staleness don't stack (Date/Age).
  • Accepted nuance: devcon-app's version-poll refetch can re-read a ≤60s-stale payload (refetch URL isn't cache-busted; the OG-card ?v= URLs are). Moot for DC8: event-app SWR-revalidates instead.
  • ✅ Edge caching ACTIVATED 2026-08-19 — via Render, not a Cloudflare rule (issue #2649): devcon.org's DNS isn't on Cloudflare, so no Cache Rule was possible; but Render's CDN is Cloudflare (api-devcon.onrender.com.cdn.cloudflare.net), and its Edge Caching → "All files (advanced)" setting (on the api-devcon service — not devcon-podbox) gives the same semantics: it honors Cache-Control and caches nothing that says no-store. To make that fail-closed, a defaultNoStore middleware (mounted app-wide in app.ts) now stamps Cache-Control: no-store on every response that didn't set an explicit policy — otherwise header-less routes (/regenerate, at-slurper GETs, devabot threads) would have inherited Render's ~120min default TTL (frozen side-effect GETs, publicly cached per-user data). Also fixed: publicCache now covers HEAD, so curl -sI probes show real headers. Verified live: /sessions MISS → HIT (age header present), 404s BYPASS. The edge cache auto-purges on every deploy, so data-sync commits still propagate normally. Note: cache HITs never reach the origin — Render request logs/metrics undercount real traffic now.
  • Optional if 60s is ever too slow: fire-and-forget Cloudflare purge in the two real-time write paths (Pretalx webhook, PUT /sessions/sources/:id); needs a cache-purge zone token in Render's env, deliberately not built yet.

12f. Changelog: Pretalx "release new version" timeouts root-caused, 2026-08-19

Releasing a schedule from the Pretalx orga UI kept timing out (and the release rolled back — no version minted), while the API release endpoint worked. Root cause is a chain across both sides of the webhook:

  • The pretalx-webhook-plugin fires its POST synchronously inside the release request (Django signals) with no timeout, and before the release transaction commits.
  • Since the release-race guard (§2), devcon-api's webhook handler held the response open for the entire sync — 10s when visibility resolves fast, up to 2+ minutes when it doesn't. Release request = freeze + that wait → nginx 60s / gunicorn 30s timeouts kill the worker → transaction rollback → "timed out, nothing released". Pre-commit firing also means a rolled-back release can still trigger a ghost sync.

Fixes:

  • devcon-api (hooks.ts): webhook now acks 204 immediately and runs SyncPretalx detached (waitForPendingSync() exposes it to tests). Deployed via normal push; this alone makes the deployed plugin harmless, since its blocking POST returns in <1s. Must stay 204 — plugin ≤0.1.5 only accepts 200/201/204.
  • Plugin 0.2.0 (committed, deploy pending): POST moved to a daemon thread scheduled via transaction.on_commit + (5, 180)s timeout — releases never block on the receiver and webhooks can no longer announce rolled-back releases. Deploy = bump the git pin in cluster/devcon/pretalx/ansible/inventories/mumbai/group_vars/instances.yaml (currently 21a8d2e; the pretalx pin itself moved on 2026-10-08, §12g) + ansible run.
  • Fallback while any of this is undeployed: pnpm pretalx:release (devcon-api) releases via the API with auto-incremented version numbers.

12g. Changelog: Pretalx upgrade, partner schedule sync, release guard, 2026-10-08 to 10-09

Context: Open Tech Summit India (FOSSASIA) runs co-located with Devcon 8 on the CLS stage and publishes its own schedule on eventyay (a Pretalx fork). Its sessions had to exist in our Pretalx event like every other talk, so the app, the slides pipeline and the run of show see them, without the partner re-entering anything.

  • Pretalx upgraded for the tags API (cluster repo, instances.yaml): the pin moved from the launch commit to upstream 071224513a (2026-03-31, six days later), the commit that fixes the submission serializer rejecting every tags write ("Invalid pk"). Deployed with ansible on both Mumbai nodes on 2026-10-08; the comment next to the pin lists the versions. Anything from v2026.2.0 upwards changes the API, so check the devcon-api clients before going further. The webhook plugin pin is unchanged (§12f).
  • pnpm sync:eventyay <event> [--apply] (sync-eventyay.ts): reads the partner's public widget JSON and creates or updates proposals on test-devcon-8 or devcon8, matched on the source session code kept in an organiser-only "Sync source ID" question. Dry run by default, idempotent, never deletes (a vanished source session is reported). It maps the partner's session types to ours (CLS - Talk / Lightning Talk / Miniworkshop / Panel (Open Tech Summit)), its rooms (CLS Stage, CLS Stage - Builders Room) and its tracks to our tags, accepts and confirms, and places each proposal in the unreleased WIP schedule with the source duration. A session held several times becomes one proposal per run with a "(n/N)" title suffix, since our events allow one slot per proposal. Speakers: the partner contacts its own speakers, so no real email is involved. A speaker whose full name already exists on devcon8 or devcon-7 is attached as that account (gaps filled, nothing overwritten); everyone else gets a placeholder address on a team mailbox with a + suffix per source speaker (SYNC_PLACEHOLDER_EMAIL in .env, never in the repo), so the profile exists with the eventyay bio, avatar (webp converted to PNG, Pretalx refuses webp) and social links (X, Telegram, one website/GitHub/LinkedIn link, into the speaker questions). Applied on devcon8 on 2026-10-08: 43 proposals on Nov 5, 66 speaker links. Pretalx queues an "added to a proposal" draft in the outbox for every attach; nothing is sent, the speaker team discards them.
  • Release guard: pnpm pretalx:release refuses devcon8 (NEVER_RELEASE, no override). Publishing Devcon 8 schedules is the speaker team's act in the orga UI (§2). The whole flow was exercised on test-devcon-8 first (release 0.35, 2026-10-08).
  • Slides for partner tracks: SLIDES_DELEGATE_TRACKS / SLIDES_PLACEHOLDER_DELEGATE (§2d). The 43 decks were created on 2026-10-08 with SLIDES_EVENTS=devcon8, each shared with the partner contact only (verified in Drive), deck links written to Pretalx.
  • Mapper (clients/pretalx.ts, all events): description falls back to the abstract when Pretalx returns an empty string (the partner's sessions carry only an abstract); the "Personal Website, Github, or other relevant link" answer is now published, as github for a GitHub URL on events without a dedicated GitHub question, as website otherwise. It was mapped but never written before, so the next sync also surfaces the links Devcon 8 speakers entered there.
  • Sync problems list now includes same-event and cross-event session-id collisions (§7.7).
  • "Featured speaker" question on devcon8 (2026-10-09, §12 DC8 configuration; replaced 2026-10-21 by #featured in the speaker's Internal notes, see above).
  • First Devcon 8 schedule release, 2026-10-09, by the speaker team in the orga UI. The webhook dispatched sync-pretalx-devcon8 (success: 316 sessions, 320 speakers, 11 rooms committed, slides off as designed, 43 collision warnings against test-devcon-8) and run-of-show-devcon8, which failed on the Google Sheets "Write requests per minute per user" quota while writing the first full schedule (four Sheets writes per tab, about 45 tabs, Google allows 60 a minute). Fixed the same day: every write is spaced to stay under the limit and retried with backoff on 429/5xx, verified with a full Devcon 8 run against a throwaway sheet (32 tabs, 3 minutes, no retries). Found on the way: both run-of-show workflows read the same repo secret ROS_SPREADSHEET_ID, so the test release of 2026-10-09 rewrote the AV team's sheet with test data before Ceci's next releases half-rewrote it; the test workflow now reads ROS_SPREADSHEET_ID_TEST_DEVCON_8, unset, so it exits before writing.
  • Meerkat ping for DC8 (#11): Meerkat now pulls the schedule from Pretalx itself and exposes one public POST .../api/v1/pretalx/<event>/sync per event (one sync a minute at most, answers 200 or 202). notifyClients() posts there for test-devcon-8 and devcon8 on every sync run (the path parameter is the Pretalx slug; Meerkat looks its conference up by it), keeps sending the bearer token when WEBHOOK_MEERKAT_SECRET is set, and both workflows now pass that secret. Verified against the test event: Meerkat answered with the released version and the upserted and deleted codes (talks that leave a release are removed on their side too). Meerkat keys sessions by Pretalx code (the app's Q&A block already tries that first) and its stage is the slug of the room name, which equals our room id: the room screens sent the room name, so the "See questions" QR never showed and the presenter link 404ed; they send the id now (RoomScreen.tsx). Meerkat has one conference, "Devcon 8 Mumbai", still linked to test-devcon-8; the prod ping answers 404 until the Meerkat team relinks it to devcon8.
  • Test mirror cleaned up (2026-10-09): the 43 Open Tech Summit proposals were deleted from test-devcon-8 through the API and test release 0.36 synced them out (11 test sessions left), so Devcon 8 syncs no longer report id collisions for them. The 43 test decks remain in the test Drive folder.

12h. Changelog: AV asset pack in the team's Drive folder, 2026-10-12 to 10-13

pnpm av:assets devcon8 [--upload] (av-assets.ts) builds and syncs what the stage crew needs offline, and runs in the devcon8 sync workflow after every schedule release (secret AV_DRIVE_FOLDER_ID_DEVCON8, the Google service account, manifest committed as data/av-assets/devcon8.json):

  • one 1920×1080 intro card per session (the /api/social/av/ render) under $$EVENT/<Room>/<Day N - Mon D>/Intro Cards/HH.MM <Title> [CODE].jpg, so files sort in running order; the AV card got the white logomark on every track and 160 px avatars on the footer baseline (dc8-social-card.tsx, AV size only);
  • one speaker photo per scheduled speaker in Speaker Assets/Photos, 1024 px from the Pretalx original, else our 256 px mirror ((256px only)), else the blockie;
  • the list of speakers without a photo as a Google Doc, rewritten in place;
  • (2026-10-20) one Q&A QR per stage, $$EVENT/<Room>/Q&A QR - <Room>.png (2048 px, from the site's own /qr/meerkat/<room>?svg), linked from each run-of-show sheet (§2e); none for the Music Stage, which has no Q&A.

The manifest keys files by what they are (card:<code>, photo:<code>, qaqr:<room>), so a rescheduled session's card is moved, a changed one rewritten in place, a stale one trashed, and links stay stable. A file that fails to render in a run (a timeout or a 403 from devcon.org) is not stale: Drive keeps the previous one (fixed 2026-10-20, after a burst of 403s trashed four cards; they were restored). Day folders are numbered over the whole event, also with --rooms.

Drive labelling, and the trick that works around it. The EF shared drive labels every file an external account uploads "EF Internal" within seconds and the label hides the file from that account: the service account could add files but never see, update or trash them again (and domain-wide delegation is not authorised for it). Copies of a Public-labelled file inherit Public and keep it when their bytes are replaced, so every file is created as a copy of _public placeholder (DON'T DELETE).png in Speaker Assets/Photos and then overwritten with the real content; Google-native files and folders are not labelled, so Docs and folders are created directly. Keep that seed image where it is; if it moves, the script searches the AV tree before giving up.

Still open after these changelogs

Blockers #2 (production devcon8 room stream fields - needs the DC8 YouTube channels), #6 second half (DC8 asset swap + DC7 location/date/timezone copy in the card templates - infra done, §12d), #9 (YouTube OAuth; #11 Meerkat closed 2026-10-09, #10 closed 2026-08-13: AV write path + token migration verified), footguns §5.2-5.3 (sync deletion, spread-order fragility; §5.1's run-of-show rebuild is fixed, §2e). §12e's edge caching is now fully active (Render Edge Caching "All files", 2026-08-19). Added 2026-10-09 (§12g): CI still creates no decks for devcon8 (§2d checklist).

Verification

No code changes are proposed in this document. To validate the findings above:

  • Coverage counts: cd monorepo/devcon-api && pnpm stats:v <eventId>.
  • Room stream fields: inspect devcon-api/data/rooms/devcon8/ vs devcon-api/data/rooms/devcon-7/main-stage.json.
  • Broken slides field: grep -rl 'slidesUrl' monorepo/devcon-api/data/sessions/ returns nothing; grep -rl 'resources_slides' returns 814 files.
  • Dormant recommender: curl https://api.devcon.org/sessions/<id>/related → 404.
  • Thumbnail rendering: curl -I https://devcon.org/api/social/av/<sourceId> (check the x-og-cache header).
  • Pipeline liveness: git log --oneline --author=github-actions -5 shows [action] Pretalx Sync commits (the first (devcon8) one landed 2026-10-09).
  • Sync auth against cfp.devcon.org: run pnpm sync:pretalx:devcon8 locally - a stale base URL / key shows as public endpoints 200 but private events 401 (the redirect strips the Authorization header, so the failure is silent).
  • Meerkat ping: grep -n "meerkat" devcon-api/src/scripts/sync-pretalx.ts shows the MEERKAT_EVENTS map (#11); curl -s --get --data-urlencode 'stage=<room id>' https://app.meerkat.events/api/v1/events lists what Meerkat holds per stage.