Skip to content

Bootcamp workshop materials on the portal - #394

Merged
aamoghS merged 4 commits into
devfrom
bootcamp
Sep 18, 2026
Merged

aamoghS merged 4 commits into
devfrom
bootcamp

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Overlaps with #390, already merged into dev. That PR added bootcamp_material — N files attached to a session event. This PR adds bootcamp_workshop — one row per week carrying a title, a materials ZIP, a solution ZIP, a recording link and a publish toggle. 13 files conflict, including schemas/bootcamp.ts, routers/bootcamp.ts, bootcamp-storage.ts, bootcamp-access.ts and both portal pages. This needs a call on which model to keep before it can merge — see "Relationship to #390" at the bottom.

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/bootcamp saw 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/bootcamp and download from there.

Member page

Room D.M. Smith 115 and time 6:30–7:30 PM, Tuesdays are 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 to event: 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 push already runs in buildCommand, and this is additive so push won't refuse it. db:check is 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 because uploadProcedure caps at 2MB and superjson base64s the body — a single tutorial notebook in dsgt-bootcamp-f26 is already 1.7MB before adding a dataset.

  • Objects are keyed by the immutable workshop id, not (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.
  • Uploads stream into the bucket rather than buffering. At concurrency: 80 on a 1GiB instance, holding a 20MB body plus its Buffer copy 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 — otherwise 404, not 403, 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 setPublished procedure 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 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 on edit would invalidate printed signs.
  • Clearing a date detaches the event rather than deleting it, because deleting an event destroys its check-ins.

Test-suite fix included

Eleven suites hand-build a vi.mock("@query/db") listing its exports. routers/bootcamp.ts references bootcampWorkshops at module scope, so every suite importing root.ts failed to collect — 363 tests passing but 11 suites dead. bootcampWorkshops (and a missing events) were added to those mocks. That accounts for the 11 unrelated-looking test files in the diff.

Verification

Gate Result
pnpm test 35/35 suites, 797 tests, 0 failures
pnpm lint 8/8 tasks, 0 errors, no suppressions
pnpm typecheck 9/9 workspaces clean
pnpm build --filter=web exit 0, all 4 bootcamp routes in the manifest

Exercised 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

  1. The runtime service account needs roles/storage.objectAdmin on gs://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 on gs://dsgt-resumes.
  2. No real ZIP has round-tripped through GCS — no credentials were available in the dev environment, so the upload path is covered by unit tests and status codes only.
  3. Confirm gs://dsgt-bootcamp has 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 is US-EAST4 while dsgt-resumes is us-central1 — correct either way, but cross-region egress applies per download.
  4. 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

#390 bootcamp_material (merged) This PR bootcamp_workshop
Model N files attached to a session event (FK, cascade) One row per week, standalone
Columns storageKey, fileName, contentType, sizeBytes, uploadedBy week, title, materials zip, solution zip, recording URL
Draft / publish none publish toggle, drafts by default
Download gate enrolment + term staff, or enrolment + term + published + metadata

#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.ts and bootcamp-access.ts, so one model has to win — either port this PR's publish/recording/week concepts onto bootcamp_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 one bootcamp_workshop row per term/week that holds title, optional recording URL, separate materials/solution ZIP metadata, and an isPublished draft 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, and upsertSession (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 under bootcamp/<workshopId>/<kind>.zip; the old multi-type upload routes and BootcampMaterials UI are removed. bootcampCaller and canDownloadBootcampFile gate proxied downloads (staff always; members only published files for their enrolled term, else 404).

Portal: /admin/bootcamp gets a workshop table + modal (create/edit, uploads, publish toggle, delete); /club/bootcamp shows the member materials table for enrolled (and past-cohort) readers. Docs, env comments, and tests are updated to match; bootcamp_material stays 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.

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread sites/mainweb/lib/bootcamp-route-rules.ts
Comment thread sites/mainweb/app/(portal)/club/bootcamp/page.tsx Outdated
Comment thread packages/api/src/routers/bootcamp.ts
@sahithrajesh sahithrajesh changed the title Feature: bootcamp to dev Bootcamp workshop materials on the portal Sep 18, 2026
aamoghS

This comment was marked as resolved.

@aamoghS aamoghS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded. We decided to keep this PR's bootcamp_workshop model rather than port it onto #390. See the comment below for what was changed on the branch.

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).
@github-actions

Copy link
Copy Markdown
Contributor Author

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@aamoghS

aamoghS commented Sep 18, 2026

Copy link
Copy Markdown
Member

Decision: this PR's bootcamp_workshop model wins over #390's bootcamp_material. I merged dev into bootcamp, resolved the conflicts and fixed the review findings in f5f6170. Please pull before pushing again.

Conflict resolution

  • Removed Add downloadable bootcamp session materials #390's routes (api/bootcamp/materials/route.ts, [id]/route.ts; [id] would also have collided with [workshopId] as a sibling dynamic segment), BootcampMaterials.tsx, bootcamp-materials.ts and their tests.
  • bootcamp_material stays declared in schemas/bootcamp.ts, marked legacy. It exists in prod, so removing the declaration would make the deploy's drizzle-kit push stop at a DROP TABLE prompt. Drop it in a separate PR.
  • Club page: kept Add downloadable bootcamp session materials #390's syllabus with attendance badges (so enrolled members see their weeks again, which addresses Bugbot's "Enrolled members lose syllabus view"), and added your welcome copy and the weekly materials table.
  • apphosting.yaml / environment.md: one BOOTCAMP_BUCKET entry. Rewrote docs/bootcamp-materials.md for the workshop model.

Fixes

  • Term gate: workshops returns the caller's own cohort (member.bootcampTerm), and canDownloadBootcampFile compares the caller's term to the workshop's term. There's no more currentTerm() in the member path, so fall material stays reachable in January.
  • Blank date detaching a live session: upsertSession now takes a workshopId and reads term and week from that row, and the admin page calls it only when the date or room actually changed. Saving a TBA workshop touches no event, and editing a past cohort can't reach the current one.
  • Title and room clobbering: rescheduling no longer sets the event title, and an omitted location leaves the event's room alone.
  • Week edits orphaning the session: week is removed from updateWorkshop and disabled in the modal when editing. To renumber a workshop, delete it and create it again.
  • Removed the process-wide setMaxListeners(15). If the GCS stream warning comes back, scope the fix to that emitter.

Verified: pnpm test passes 37/37 suites (825 tests), including new cases for past-cohort access, the row's term in upsertSession, an omitted location, and a missing workshop. Lint is 8/8. build --filter=web passes, with both [workshopId] routes in the manifest. Typecheck is clean except packages/db/scripts/seed-club-projects.ts (duplicate SchemaModule), which is already broken on dev.

Infra: the service account and bucket items are done. firebase-app-hosting-compute@dsgt-website.iam.gserviceaccount.com has objectAdmin on gs://dsgt-bootcamp (the same account as the resume bucket), and the bucket is US-EAST4 with uniform access and public access prevention enforced. App Hosting runs in us-east4 too.

Still open: no real ZIP has gone through GCS end to end yet. Upload one test ZIP on a preview deploy before announcing.

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/api/src/routers/bootcamp.ts Outdated
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.
@aamoghS
aamoghS self-requested a review September 18, 2026 19:41

@aamoghS aamoghS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to go

@aamoghS
aamoghS merged commit f2cfde7 into dev Sep 18, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants