Conversation
Officers had no way to publish weekly bootcamp material without a deploy: the curriculum was a frontend constant and the notebooks lived in a separate repo and on Deepnote. Enrolled members opening /club/bootcamp saw only a "work in progress" card. Admins now manage a workshop table at /admin/bootcamp — week, title, session date, materials ZIP, solution ZIP and a recording link — and publish rows when they are ready. Enrolled members see the published rows at /club/bootcamp and download from there. Schema - New bootcamp_workshop table, keyed unique on (term, week), drafts by default. No FK to event: deleting a session must not take the notebooks with it, and material gets posted for weeks whose event row was never created. Storage - ZIPs live in Cloud Storage (BOOTCAMP_BUCKET); Postgres holds metadata and the object key, mirroring the resume book. Bytes move over plain route handlers because uploadProcedure caps at 2MB and superjson base64s the body — a single tutorial notebook already exceeds that. - Objects are keyed by the immutable workshop id rather than (term, week), so editing a week cannot leave a row pointing at another row's file. - Uploads stream into the bucket instead of buffering: at concurrency 80 on a 1GiB instance, holding a 20MB body plus its copy per request is not safe. Access - Writes gate on isAdmin. Downloads require staff, or all of: enrolment, current term, published, and non-null metadata for that kind — otherwise 404, so a guessed workshop id cannot confirm a draft exists. - Downloads are proxied, never redirected to a signed URL, which would leave the origin and the auth check behind. Session dates - Setting a date upserts the underlying bootcamp event for that (term, week), so the date has one source of truth and the session also gets attendance and QR check-in. The QR code is minted on insert only — regenerating it would invalidate printed signs. Clearing a date detaches the event rather than deleting it, which would destroy its check-ins. Also - Member page states the real room and time and drops the placeholder copy. - Adds bootcampWorkshops to the eleven test suites that hand-build a @query/db mock; without it every suite importing root.ts failed to collect. - Corrects the comments in bootcamp-schedule.ts and schemas/events.ts that asserted a curriculum is never edited and that a bootcamp session has no table.
Resolve the #390 overlap in favour of bootcamp_workshop. #390's routes, components and tests are removed; its table stays declared so drizzle-kit push does not stop at a DROP TABLE prompt, to be dropped separately. Club page keeps #390's syllabus with attendance badges and adds the welcome copy and weekly materials table, so enrolled members see both. Fixes from review: - Gate member reads and downloads on the workshop's own term, not currentTerm(), so a cohort keeps its material after the term rolls. - upsertSession takes a workshopId and uses that row's term and week, and the admin page calls it only when the date or room changed, so a TBA save no longer detaches the live session for that week. - Rescheduling no longer overwrites the event's title, and an omitted location leaves the event's room alone. - A workshop's week is fixed after creation; the session joins on it. - Drop the process-wide setMaxListeners(15).
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Decision: this PR's Conflict resolution
Fixes
Verified: Infra: the service account and bucket items are done. Still open: no real ZIP has gone through GCS end to end yet. Upload one test ZIP on a preview deploy before announcing. |
|
Visit the preview URL for this PR (updated for commit ecf26e0): https://hacklytics2027--pr-394-ti907olm.web.app (expires Fri, 25 Sep 2026 18:27:24 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c48ba34db61581e25fe2978355160b5eefe0e83f |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f5f6170. Configure here.
Bugbot on f5f6170: the admin screen can be filtered to a past cohort, but createWorkshop always stamped currentTerm(). Adding a week while viewing 2025-fall filed the row under the live term, where it vanished from the table on refresh and could collide with that term's existing week. createWorkshop now takes the term the officer is viewing. It is still refused unless the bootcamp actually ran in that term — an admin-supplied string would otherwise file material under a semester nobody is enrolled in, which is the failure the derive-the-term rule in events.ts exists to prevent. The format is validated too, so a typo cannot reach the unique index. Also removes a duplicated `import type * as SchemaModule` in scripts/seed-club-projects.ts. Unrelated to this feature and identical to dev, but it fails @query/db typecheck and so would have kept CI red on this PR.
Next 16 writes these into sites/mainweb on every dev run and build. They were swept into the previous commit by git add -A; gitignore them so they stay out.

Officers had no way to publish weekly bootcamp material without a deploy — the curriculum was a frontend constant and the notebooks lived in a separate repo and on Deepnote. An enrolled member opening
/club/bootcampsaw a "work in progress" card and nothing else, because the syllabus sat in the branch that only renders for people who aren't enrolled.Admins manage a workshop table at
/admin/bootcamp— week, title, session date, materials ZIP, solution ZIP, recording link — and publish rows when they're ready. Enrolled members see the published rows at/club/bootcampand download from there.Member page
Room
D.M. Smith 115and time6:30–7:30 PM, Tuesdaysare set, the placeholder copy is gone, and the heading reads "Welcome to the Fall 2026 Bootcamp".The enrolled and not-enrolled branches were mutually exclusive, so an enrolled member saw only the status card. The enrolled arm now renders the materials table too — without this the feature would have been visible to everyone except the students it's for.
Schema
bootcamp_workshop, unique on(term, week), drafts by default. No FK toevent: deleting a session must not take the notebooks with it, and material gets posted for weeks whose event row was never created.Created automatically on deploy —
drizzle-kit pushalready runs inbuildCommand, and this is additive so push won't refuse it.db:checkis columns-only, so the unique constraint and index aren't covered by it; both were verified by hand against a real Postgres.Storage
ZIPs go to Cloud Storage (
BOOTCAMP_BUCKET); Postgres holds metadata and the object key, mirroring the resume book. Bytes move over plain route handlers becauseuploadProcedurecaps at 2MB and superjson base64s the body — a single tutorial notebook indsgt-bootcamp-f26is already 1.7MB before adding a dataset.(term, week). With a week-based key, editing a row's week leaves it pointing at the old object and a new row for that week silently overwrites it — then deleting either row destroys the other's file.concurrency: 80on a 1GiB instance, holding a 20MB body plus itsBuffercopy per request is not safe. Cap is 20MB; Cloud Run's HTTP/1 ceiling is 32MiB, so that's headroom, not the platform limit.Access control
Writes gate on
isAdmin. Downloads require staff, or all of: enrolment, current term, published, and non-null metadata for that kind — otherwise404, not403, so a guessed workshop id can't confirm a draft exists. "Enrolled or staff" alone would let any enrolled member pull drafts and past-term material.Downloads are proxied, never redirected to a signed URL, which would leave the origin and the auth check behind.
Publishing is a separate
setPublishedprocedure rather than a field on the save. Riding along with the edit would let a row go visible before its uploads landed, and stay published if one failed.Session dates
Setting a date upserts the underlying bootcamp
eventfor that(term, week), so the date has one source of truth and the session also gets attendance and QR check-in.Test-suite fix included
Eleven suites hand-build a
vi.mock("@query/db")listing its exports.routers/bootcamp.tsreferencesbootcampWorkshopsat module scope, so every suite importingroot.tsfailed to collect — 363 tests passing but 11 suites dead.bootcampWorkshops(and a missingevents) were added to those mocks. That accounts for the 11 unrelated-looking test files in the diff.Verification
pnpm testpnpm lintpnpm typecheckpnpm build --filter=webExercised against a real local Postgres, not only mocks: member view returns published rows only while admin also sees the draft; setting a date creates the event and surfaces it in both views; re-saving changes the date and leaves the QR untouched; publish toggles both ways.
Not yet done
roles/storage.objectAdminongs://dsgt-bootcamp. Creating the bucket does not grant it. Until then every upload and download returns 502 while the rest of the feature works. Mirror whichever SA holds that role ongs://dsgt-resumes.gs://dsgt-bootcamphas uniform bucket-level access and public-access prevention. These are student notebooks, and the proxied download is undermined if the bucket is directly readable. It isUS-EAST4whiledsgt-resumesisus-central1— correct either way, but cross-region egress applies per download.graphify update .after merge — the committed graph predates the portal bootcamp pages and points at the public marketing page.Known limitation
A concurrent upload and remove of the same file can race, since the object write and row update can't be one transaction. Low impact with a handful of officers, but real.
Relationship to #390
bootcamp_material(merged)bootcamp_workshop#390 is the cleaner generic attachment model and ties naturally to events. It has no publish toggle and no recording column, which were explicit requirements here. The two can't both own
bootcamp-storage.tsandbootcamp-access.ts, so one model has to win — either port this PR's publish/recording/week concepts ontobootcamp_material, or reconcile the conflicting files in favour of this one.Note
Medium Risk
Touches member file access, admin publish state, and GCS upload/download paths; it also supersedes the merged #390 handout model, so integration and bucket IAM must be correct before deploy.
Overview
Replaces per-session handout files (
bootcamp_material) with onebootcamp_workshoprow per term/week that holds title, optional recording URL, separate materials/solution ZIP metadata, and anisPublisheddraft gate. Workshop rows join session dates on(term, week)without an event FK so deleting or skipping a session does not drop published notebooks.API and storage: The bootcamp tRPC router gains
workshops,adminWorkshops, CRUD/publish mutations, andupsertSession(create/reschedule QR-backed events, or detach on clear). File I/O moves to/api/bootcamp/materials/[workshopId]/[kind]with streamed 20MB ZIP validation and keys underbootcamp/<workshopId>/<kind>.zip; the old multi-type upload routes andBootcampMaterialsUI are removed.bootcampCallerandcanDownloadBootcampFilegate proxied downloads (staff always; members only published files for their enrolled term, else 404).Portal:
/admin/bootcampgets a workshop table + modal (create/edit, uploads, publish toggle, delete);/club/bootcampshows the member materials table for enrolled (and past-cohort) readers. Docs, env comments, and tests are updated to match;bootcamp_materialstays declared but unused until a follow-up drop.Reviewed by Cursor Bugbot for commit ecf26e0. Bugbot is set up for automated code reviews on this repo. Configure here.