|
| 1 | +# Bootcamp materials |
| 2 | + |
| 3 | +Each week of a bootcamp has one workshop row: a title, a materials ZIP, a solution ZIP and a recording link. Staff manage the rows at `/admin/bootcamp` and publish each one when it's ready. Members download from `/club/bootcamp`. Nobody else can download, including members who didn't buy the bootcamp. |
| 4 | + |
| 5 | +Sessions themselves are ordinary events carrying `bootcamp_week` and `bootcamp_term`; see [Architecture](./architecture.md). A workshop row finds its session by `(term, week)`. There is no foreign key, so deleting a session never deletes the notebooks. |
| 6 | + |
| 7 | +## Where things are |
| 8 | + |
| 9 | +| Piece | Path | |
| 10 | +| --- | --- | |
| 11 | +| Table (metadata only) | `packages/db/src/schemas/bootcamp.ts` (`bootcamp_workshop`) | |
| 12 | +| API | `packages/api/src/routers/bootcamp.ts` (`workshops`, `adminWorkshops`, `createWorkshop`, `updateWorkshop`, `upsertSession`, `setPublished`) | |
| 13 | +| Upload / serve / clear one ZIP | `sites/mainweb/app/(portal)/api/bootcamp/materials/[workshopId]/[kind]/route.ts` | |
| 14 | +| Delete a workshop | `sites/mainweb/app/(portal)/api/bootcamp/materials/[workshopId]/route.ts` | |
| 15 | +| ZIP rules (signature, cap, names) | `sites/mainweb/lib/bootcamp-file.ts` | |
| 16 | +| Download rule | `sites/mainweb/lib/bootcamp-route-rules.ts` | |
| 17 | +| Caller lookup | `sites/mainweb/lib/bootcamp-access.ts` | |
| 18 | +| Bucket client | `sites/mainweb/lib/bootcamp-storage.ts` | |
| 19 | +| Member table / staff modal | `sites/mainweb/components/portal/BootcampMaterialsTable.tsx`, `BootcampWorkshopModal.tsx` | |
| 20 | + |
| 21 | +## Who can download a file |
| 22 | + |
| 23 | +- Staff can download anything, drafts and past terms included. |
| 24 | +- A member can download a **published** file when their `member.bootcamp_term` equals the **workshop's term**, not the current term. Someone who bought the fall bootcamp keeps the fall notebooks in January. They don't get the spring ones. |
| 25 | +- Everyone else gets **404, not 403**, so a guessed workshop id can't confirm that a draft exists. |
| 26 | + |
| 27 | +Publishing is its own procedure (`setPublished`), separate from saving. That keeps a row hidden until its uploads have landed, and a failed upload can't leave it visible. |
| 28 | + |
| 29 | +## Session dates |
| 30 | + |
| 31 | +The modal's date and room fields call `upsertSession`, but only when the officer changed one of them. So saving a TBA workshop never touches an event. Term and week come from the workshop row, never from the clock, so editing a past cohort can't reach the current one. |
| 32 | + |
| 33 | +- **Setting a date** creates the week's event, or reschedules the existing one. The QR code is minted on insert only, so printed signs keep working. The event's title is left alone on reschedule. |
| 34 | +- **Clearing a date** detaches the event (clears `bootcamp_week`/`bootcamp_term`) rather than deleting it, because deleting an event destroys its check-ins. |
| 35 | +- A workshop's **week is fixed** once it's created, because moving it would leave its session behind. To renumber a workshop, delete it and create it again. |
| 36 | + |
| 37 | +## Storage |
| 38 | + |
| 39 | +ZIPs live in `gs://dsgt-bootcamp` under `bootcamp/<workshopId>/<kind>.zip`. Postgres holds the object key, file name and size. Objects are keyed by the workshop id, which never changes. A key based on `(term, week)` would let one row's upload overwrite another row's file. |
| 40 | + |
| 41 | +Uploads bypass tRPC and stream straight into the bucket. `uploadProcedure` caps at 2MB and superjson base64-encodes the body. The first four bytes are held until the ZIP signature checks out, and the stream stops at 20MB, which leaves headroom under Cloud Run's 32MiB request limit. |
| 42 | + |
| 43 | +Downloads are proxied, never redirected to a signed URL, which would leave the origin and the auth check behind. The bucket keeps uniform bucket-level access and public access prevention **enforced**. The App Hosting runtime service account has `roles/storage.objectAdmin` on it. |
| 44 | + |
| 45 | +Credentials are Application Default Credentials. Locally, run `gcloud auth application-default login`. With `BOOTCAMP_BUCKET` unset, the file routes return 503. |
| 46 | + |
| 47 | +## Known limitation |
| 48 | + |
| 49 | +An upload and a removal of the same file can race, because the object write and the row update can't be one transaction. With a handful of officers, the impact is low. |
| 50 | + |
| 51 | +## Legacy table |
| 52 | + |
| 53 | +`bootcamp_material` (#390's per-event handouts) is still declared in the schema, but nothing reads or writes it. Deploys run `drizzle-kit push`, and removing the declaration would make push stop at a DROP TABLE prompt. Drop the table in a separate change. |
0 commit comments