Skip to content

feat(programs): Denver programs + Past-programs surface + public projects from signups#144

Merged
sacha-l merged 6 commits into
developfrom
feat/denver-programs
May 22, 2026
Merged

feat(programs): Denver programs + Past-programs surface + public projects from signups#144
sacha-l merged 6 commits into
developfrom
feat/denver-programs

Conversation

@sacha-l
Copy link
Copy Markdown
Collaborator

@sacha-l sacha-l commented May 21, 2026

Summary

Three related pieces for surfacing past programs and what was built/pitched at them.

  1. Denver programs — repurpose dogfooding-2026-berlinDogfooding 2026 Denver (dogfooding-2026-denver, completed, luma.com/dogfooding copy); add PitchOff! Denver 2026 (pitch_off, completed, 2026-02-21).
  2. Past-programs surface/programs now fetches all programs and renders a "Past programs" section below the open grid; the rack card badge reflects status (OPEN / COMPLETED / CLOSED) instead of a hardcoded OPEN.
  3. Public PROJECTS section/programs/:slug shows the distinct projects attendees picked (+ interest counts), aggregated from the program's signups, PII-free.

The PROJECTS feature (new)

  • Endpoint: GET /programs/:slug/projectspublic, no auth. Returns [{ project, count }] sorted by count desc. Raw signups stay admin-gated; only this safe aggregate is public.
  • Aggregation (programSignupService.projectSummaryByProgramId): groups program_signups by the raw_row column whose header matches /project/i, counting distinct values. No attendee fields are ever returned.
  • Client: api.listProgramProjects(slug) with a mock branch; a representative mockProgramSignups['pitchoff-2026-denver'] fixture so the preview renders. ProgramDetailPage shows a PROJECTS panel when non-empty (hidden otherwise).
  • PII: only project name + interest count are exposed; attendee name/email/Telegram never leave the admin gate. Verified by a tester scenario.

How submissions get there (already shipped, not in this PR)

Admins upload/update each program's CSV at /admin/programs/:slug (ProgramSignupsSection: file picker → dry-run preview → commit → list/delete). This PR makes the public projection of that data visible.

Deferred → #143

  • Importing the real PitchOff! submissions CSV (blocked on the file from @sacha) — the mock fixture stands in until then.
  • Any richer custom PitchOff! layout beyond the projects list.

Decisions (flag if wrong)

  • status = completed for both Denver programs (matches symbiosis-2025). Say the word for closed.
  • Denver dogfooding event dates left null (no signal); PitchOff event date 2026-02-21 from the export timestamps.

Files changed

  • client/src/lib/mockPrograms.ts — Denver + PitchOff entries; mock signups fixture; projectSummaryFromMockSignups.
  • client/src/lib/mockProgramApplications.ts — repoint programId.
  • client/src/lib/api.tsApiProgramProject + listProgramProjects.
  • client/src/pages/ProgramsPage.tsx — Past-programs section; status-aware badge.
  • client/src/pages/ProgramDetailPage.tsx — PROJECTS section.
  • server/api/services/program-signup.service.jsprojectSummaryByProgramId.
  • server/api/controllers/program.controller.jslistProjects.
  • server/api/routes/program.routes.js — public GET /:slug/projects.
  • server/api/services/__tests__/program-signup.service.test.js — new aggregation tests.
  • server/scripts/seed-dogfooding-program.js — seeds the Denver dogfooding row.

Manual Supabase rollout (prod)

(unchanged from before — repurpose the Berlin dogfooding row to Denver, insert PitchOff!; SQL below)

UPDATE programs SET
  id='dogfooding-2026-denver', slug='dogfooding-2026-denver', name='Dogfooding 2026 Denver',
  status='completed', location='Denver', event_url='https://luma.com/dogfooding',
  applications_open_at=NULL, applications_close_at=NULL, event_starts_at=NULL, event_ends_at=NULL,
  description='Whether you''re new to web3 or deep in it — when''s the last time you actually used a new product, not just heard about one? Dogfooding Denver showcased 3–4 products built by WebZero hackathon teams. No jargon-filled panels, no slideshows — just hands-on time with real apps. Attendees picked a product, spent 30 minutes on guided tasks, and submitted feedback that went directly to the builders. No technical background or wallet required — just curiosity. The feedback shapes what gets built next.'
WHERE id='dogfooding-2026-berlin';

INSERT INTO programs (id,name,slug,program_type,description,status,owner,location,event_starts_at,event_ends_at)
VALUES ('pitchoff-2026-denver','PitchOff! Denver 2026','pitchoff-2026-denver','pitch_off',
  'A live pitch event in Denver where WebZero builders presented their projects to the room. Attendees signed up, chose the projects they were most excited about, and could opt into the bounty round.',
  'completed','webzero','Denver','2026-02-21T00:00:00Z','2026-02-21T23:59:59Z');

Then import the PitchOff! signups CSV at /admin/programs/pitchoff-2026-denver to populate the public PROJECTS list.

Test plan

  • npm test (server) — 252 passed (4 new aggregation tests).
  • npm run build (client) — green.
  • npm run lint (client) — clean.
  • stadium-tester @ localhost (mock) — Past-programs 8/8, PROJECTS section 5/5 (incl. a PII-leak check + empty-state check).
  • Visual review on the Vercel preview.
  • Supabase rollout + CSV import after merge.

stadium-tester report (PROJECTS)

**Scenarios**: 4 + sanity, all PASS
| # | Scenario                                                      | Result |
|---|---------------------------------------------------------------|--------|
| 1 | PitchOff! page shows a PROJECTS section                        | PASS   |
| 2 | projects render w/ counts, top project first (4 INTERESTED)    | PASS   |
| 3 | no attendee PII (email / "Attendee 1") on the public page      | PASS   |
| 4 | program without signups shows no PROJECTS section              | PASS   |

window.__STADIUM_MOCK__ = true · console errors: 0

Invariants respected

  • New public route exposes only aggregates; raw signups remain requireProgramAdmin.
  • No console.* (client); dark mode only; reused tokens.
  • No Mongoose in server/api/**; no new Supabase script.
  • ESM; BYPASS_ADMIN_CHECK untouched.

Per CLAUDE.md §6: draft, never merging.

…st-programs surface

- Repurpose the dogfooding-2026-berlin fixture/seed as Dogfooding 2026 Denver
  (slug dogfooding-2026-denver, status completed, luma.com/dogfooding copy).
- Add a PitchOff! Denver 2026 program (program_type pitch_off, completed).
- Re-point the mock Plata Mia application to the Denver dogfooding id.
- ProgramsPage now fetches all programs and renders a 'Past programs' section
  below the open grid; the rack card badge reflects status
  (OPEN / COMPLETED / CLOSED) instead of a hardcoded OPEN.

Event dates for the Denver dogfooding are left null pending confirmation.
PitchOff! submissions + a custom detail layout are tracked as a follow-up.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stadium Ready Ready Preview, Comment May 22, 2026 12:40am

…om signups

Renders the distinct projects attendees picked (+ interest counts) on the
public /programs/:slug page, aggregated from program_signups without
exposing any attendee PII.

- New public endpoint GET /programs/:slug/projects (no auth) backed by
  programSignupService.projectSummaryByProgramId, which groups signups by
  the raw_row column whose header matches /project/i and returns
  [{ project, count }] sorted by count desc.
- Client api.listProgramProjects + mock aggregation; mock PitchOff signups
  fixture so the preview renders the section.
- ProgramDetailPage shows a PROJECTS panel when the aggregate is non-empty.
- Vitest covers the aggregation (grouping, sort, trimming, PII-free shape).
@sacha-l sacha-l changed the title feat(programs): Denver programs (Dogfooding + PitchOff!) + Past-programs surface feat(programs): Denver programs + Past-programs surface + public projects from signups May 21, 2026
… surrogate

The signup CSV parser now handles form exports that collect a Telegram
handle instead of an email (e.g. PitchOff!). When no email column exists,
it derives a stable surrogate identity '<handle>@telegram.imported' from
the Telegram column so rows import + dedupe under the existing NOT NULL
email + UNIQUE(program_id, email) schema — no migration. Email-column
exports (Luma) are unchanged.

- luma-csv.parser.js: TELEGRAM_HEADERS + broader name/timestamp synonyms;
  EMAIL mode vs TELEGRAM-surrogate mode chosen from the headers; telegram
  returned on each row (also preserved in raw_row).
- Mock importer (preview): mirror the surrogate logic AND capture raw_row
  (previously dropped), so a preview import populates the public PROJECTS
  aggregate just like the server.
- Tests: 3 new parser cases (surrogate, unusable-handle skip, email wins).

Note: the exact 'which project' column is matched heuristically (/project/i);
pin it once the real CSV headers are known (#143).
The submissions are per-project builder entries (name, what they built,
repo, docs, tools), not vote tallies. Pivot the public projects feature
from count-aggregation to project cards.

- projectCardsByProgramId: one signup row -> one public-safe card; fields
  detected from raw_row by header keyword (title/description/repo/docs/
  tags), PII columns ignored, non-URL links and empty cards dropped.
- GET /programs/:slug/projects returns cards; ProgramDetailPage renders
  name + blurb + REPO/DOCS links + tool tags.
- Mock fixture seeded with a BEST-EFFORT transcription of the PitchOff!
  Denver responses (names + tools as legible; descriptions + repo/doc URLs
  left blank pending the real CSV). Preview-only; prod unaffected.
- Tests updated for the card shape (PII-free, URL validation, fallbacks).

Real data import + exact column pinning tracked in #143.
@sacha-l
Copy link
Copy Markdown
Collaborator Author

sacha-l commented May 22, 2026

Update: projects pivoted from vote-counts → builder project cards.

The PitchOff! submissions turned out to be per-project builder entries (name, what they built, GitHub repo, README/doc link, tools), not vote tallies. So the public PROJECTS section now renders project cards (name · blurb · REPO/DOCS links · tool tags) instead of "N interested".

  • GET /programs/:slug/projects now returns [{ name, description, repoUrl, docsUrl, tags }] — one signup row → one public-safe card; PII columns (telegram/email/contact) are never read; non-URL links + empty cards dropped.
  • Mock fixture seeded with a best-effort transcription of the responses screenshot (names + tools as legible; descriptions + repo/doc URLs left blank pending the real CSV). Preview-only — prod stays empty until the real import (feat: import real PitchOff! submissions CSV + pin the project column #143).
  • Server 256 tests green; stadium-tester 5/5 incl. a PII-leak check and empty-state check.

…cards

Real responses CSV headers pinned end-to-end:
- parser: detect 'Team member name(s)' as name and 'Main Telegram contact'
  as the surrogate identity (norm now strips parens); + submit/start date
  synonyms. New test locks the real header shape.
- card detector: repo regex now owns the 'Github link or demo URL' column
  (added \bdemo) and docs no longer matches it, so docs maps to the
  'README or project doc link' column; tag split no longer breaks on '/'.
- mock fixture: replaced placeholders with the 10 real builder submissions,
  public-safe fields only (name, what-they-built, repo, docs, tools) — no
  Telegram/contact. file:// and 'NA' links correctly render as no-link.

Verified: server 257 tests; stadium-tester 5/5 (real names + descriptions +
repo/docs links + tags render; no PII). Closes the data side of #143.
The public program page shows only project details + contributor names.
Everything else from a submission (Telegram contact, prompt choice, dates,
network id, etc.) is admin-only — it's stored in raw_row but wasn't shown.

- Each admin signup row is now expandable (·DETAILS) into a key/value view
  of every raw_row field, so admins can see the full submission incl. the
  real Telegram contact (the public/email column is a surrogate for
  email-less Tally/Typeform imports, now labelled 'TELEGRAM IMPORT').
- Row header leads with NAME; import hint updated (Luma + Tally/Typeform,
  email OR telegram identity, extra columns kept admin-only).
@sacha-l sacha-l marked this pull request as ready for review May 22, 2026 00:42
@sacha-l sacha-l merged commit 74df8d9 into develop May 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant