Skip to content

Promote develop → main: programs, Denver events, SoundCloud, admin detail#145

Merged
sacha-l merged 19 commits into
mainfrom
develop
May 22, 2026
Merged

Promote develop → main: programs, Denver events, SoundCloud, admin detail#145
sacha-l merged 19 commits into
mainfrom
develop

Conversation

@sacha-l
Copy link
Copy Markdown
Collaborator

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

Summary

Promotes 19 commits from develop to main (production branch). Draft — review before merging; do not expect this to update prod on its own (see Deploy note).

Highlights since main (#126)

⚠️ Deploy note (important)

Production currently serves a build older than mainmain already has the /api/programs router but the live server returns 404 for it. So merging this PR will not make prod live by itself; the Railway server and Vercel client must actually redeploy from the updated branch. Please verify the Railway service's tracked branch + trigger a deploy after merge.

Data already in production Supabase

The Denver program rows, the #137 copy fixes, and all 10 PitchOff! signups are already written to the prod Supabase project (hxojfhlrtffcvksxkvwf). Once the server redeploys with this code, /programs + the project cards + admin signup detail will render immediately — no further data step needed.

Post-deploy verification

  • GET /api/programs/pitchoff-2026-denver/projects → 200 with project cards (currently 404).
  • /programs → Past programs shows both Denver events (COMPLETED).
  • /programs/pitchoff-2026-denver → project cards (names + repo/docs/tags, no contact info).
  • /admin/programs/pitchoff-2026-denver → 10 signups; expand a row → full submission incl. Telegram.

Risk

Large promotion. All constituent PRs (#136, #141, #142, #144) were reviewed + tested individually (server tests, client build/lint, stadium-tester). No squash — preserves history.

sacha-l added 19 commits May 21, 2026 11:16
Promoted via gh issue create:
- #127 express-rate-limit
- #128 helmet
- #129 ADMIN_SESSION_SECRET startup validation
- #130 req.user.chain capture in admin middlewares
- #131 audit-log on payment / payout / continuation
- #132 tsconfig.app.json strict-mode tightening (phased)
- #133 server boot smoke test
- #134 shared csvCell util for future CSV exports
…romoted

docs(backlog): mark 8 audit entries promoted to #127-#134
User reported that signing into the Dogfooding program as an admin showed
'You need to be a team member on a Stadium project to apply' — even though
the server (requireTeamMemberOrAdminByBodyProject) does let admins through.
The block was purely client-side: ProgramDetailPage only let users open the
apply modal if they were on a project's team list.

The server-side gate already accepts admins as long as the request body
references a real project_id. So the client just needs to give admins a way
to pick which project they're applying on behalf of.

Changes:
- Detect admin status via the existing isAdmin() helper (env-configured
  VITE_ADMIN_ADDRESSES list).
- When admin, fetch the full project list (api.getProjects({ limit: 500 }))
  in addition to the wallet's own team-membership query.
- The 'must be a team member' gate now only fires for non-admin users.
- Apply button copy switches to 'APPLY ON BEHALF OF…' when the admin has no
  team membership; a small 'ADMIN MODE' hint sits below the button so it's
  obvious why the picker shows everything.
- Modal mount + projects prop both read from a unified projectsForApply
  derived list (admin gets allProjects, regular user gets myProjects).

Verified:
- npm run lint: 0 warnings
- npm run build (tsc + vite): clean
- Server-side gate unchanged — requireTeamMemberOrAdminByBodyProject
  already accepts admins, so the API surface posture is preserved.
…llet rate-limit

User reported 'rate limit exceeded' and 'Couldn't load admins' toasts when
opening AdminProgramPage (e.g. /admin/programs/bitrefill-2026). Root cause:
the page mounts 4 admin sections (Admins, Sponsors, Signups, AuditLog) and
each section calls getAdminBearerHeaders() in its own mount effect. When the
admin session cache is empty, all four cache-misses race and each launches a
separate provider.signIn() — Talisman / Polkadot-JS sees 4 SIWS sign requests
in a <50ms burst and returns 'rate limit exceeded'.

Fix: store the in-flight sign promise on a ref so concurrent callers await
the same operation instead of starting parallel signs. After the first one
resolves, the bearer is cached and later callers hit the cache path.

Effect:
- One wallet popup per session (was up to 4 on AdminProgramPage)
- 'Rate limit exceeded' toast no longer fires from the burst
- No behavior change for non-concurrent callers (single ProgramAdminsSection,
  one-off admin mutations, etc.)

Verified: lint clean, tsc + vite build clean.
…lf-of-any-project

fix(admin): admin apply-on-behalf-of + wallet sign rate-limit dedupe
…imary nav

Closes #137, closes #138.

- Hero <p> on / now reads 'Stuff people have built here.'
- M2 Incubator description in mockPrograms updated to the mentorship/milestone framing.
- Generic Dogfooding description appends the ongoing-application CTA.
- M2 tab removed from the primary nav (TABS in Navigation.tsx); /m2-program route is preserved.

Mock fixtures only. Supabase prod rows for M2 / generic Dogfooding need a manual description update via the dashboard.
…m2-nav

copy + nav: refresh homepage hero, m2/dogfooding blurbs, drop m2 tab (#137, #138)
…y default)

Closes #139.

- Hidden SoundCloud HTML5 Widget iframe for soundcloud.com/pommeshdrms,
  always mounted (outside the collapsed/expanded branches) so audio
  survives the rack's auto-collapse after the first interaction.
- On widget READY: setVolume(0) then play() so muted autoplay works
  within browser autoplay policies.
- Mute/unmute toggle in the expanded view's first row, between the %
  LCD and the collapse chevron. Volume2/VolumeX lucide icons matching
  the chevron's lcd styling. aria-pressed reflects state.
- iframe allow='autoplay; encrypted-media' to suppress Chromium's
  encrypted-media permissions-policy warning emitted by SoundCloud's
  widget EME registration.
- Lazy-loaded SC widget script with idempotent guard.
Moves the mute/unmute toggle out of the brightness row into a dedicated
AUDIO row at the bottom of the rack (mirrors BRIGHTNESS / AUTO / PALETTE).

- 'pommeshdrms' artist name displayed.
- SC link → https://soundcloud.com/pommeshdrms.
- IG link → https://www.instagram.com/pommes_hdrms (matching handle).
- Both external links open in a new tab with rel='noopener noreferrer'.
- Mute toggle keeps its existing aria-pressed / aria-label contract.
…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.
Replace favicon set with a minimal three-pixel mark on a mid-grey
background, designed on a 16x16 grid for crisp small-size rendering.
Adds favicon.svg, fixes broken .public/ paths in index.html, and
fills in site.webmanifest name/theme.
…dCloud

Pulls metadata via the widget's getCurrentSound and renders a dim,
truncated 'title · genre' tag next to the artist name. Refreshed on the
widget READY event and on each PLAY (track change). Falls back to nothing
when the widget is offline or a track has no genre.
…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).
… 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.
…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).
feat(brightness-rack): minimal SoundCloud audio player, muted by default (#139)
feat(programs): Denver programs + Past-programs surface + public projects from signups
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 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:59am

@sacha-l sacha-l marked this pull request as ready for review May 22, 2026 01:00
@sacha-l sacha-l merged commit 91ace92 into main 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