Skip to content

Release v1.0.0 — promote develop → main#166

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

Release v1.0.0 — promote develop → main#166
sacha-l merged 44 commits into
mainfrom
develop

Conversation

@sacha-l
Copy link
Copy Markdown
Collaborator

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

Stadium v1.0.0 — first official release

Promotes developmain (production deploy to Railway + Vercel). Full notes in CHANGELOG.md.

What ships in v1.0.0

Stadium becomes a recurring-events platform for WebZero's builder programs (multi-chain, not Polkadot-only): hackathons, M2 incubator, dogfooding, PitchOff — where builders show their work, the community explores it, and organizers run each event end to end.

Highlights since the last promotion (#145):

PRs included: #146#165.

After merge

  • Tag v1.0.0 on main.
  • Publish the GitHub Release from the CHANGELOG body (this makes the nav v1.0 link resolve).

⚠️ Merging deploys to production.

sacha-l added 30 commits May 22, 2026 03:06
Deploys are automatic on merge to main (Railway + Vercel GitHub
integrations), so the manual one-command path is obsolete — and dangerous.

- Delete server/scripts/deploy-all.js: it re-ran stale one-off Symbiosis/
  Plata-Mia prod data mutations (whose db:* npm scripts no longer exist),
  did supabase db push, and auto-committed/pushed main.
- Remove the deploy:all script from server/package.json.
- Rewrite the PRODUCTION_DEPLOYMENT.md top section to document the
  auto-deploy-on-merge flow + the manual Supabase data step.
- Fix stale defaults: verify scripts' FRONTEND_URL and client/.env.example's
  prod API host now point at stadium.joinwebzero.com / stadium-production-996a.
- CLAUDE.md: drop deploy:all from the operational list; note auto-deploy.
…oling

chore: remove stale/dangerous deploy-all.js + fix stale prod URLs
'Units' read oddly; projects/entries are now labelled ENTRY / ENTRIES.
User-facing strings only — internal identifiers (UnitCard, unit-card.tsx,
toUnit, unitNumber, UnitForCard, UnitDetailModal) are unchanged.

Covers card labels, the detail modal header, 'NOW SHOWING / ENTRY NNN',
M2 'ENTRY / TEAM' + 'NO ENTRIES MATCH' + row labels, project-detail
breadcrumb, admin card label, the 'N ENTRIES' counts, the 'Total Entries'
stat, search placeholders, and the 404 message.
copy: rename user-facing 'UNIT' label to 'ENTRY'
… view

Makes the landing page an entry point to all WebZero programs and lets a
program show its projects.

- New ProgramSpaces component: 4 evergreen type 'spaces' (Hackathons, M2
  Incubator, Dogfooding, PitchOff) with icon + blurb + per-type event count
  + a culture blurb, in the existing rack/LCD aesthetic. Rendered on HomePage
  after the stats panel; reuses the programs already fetched there.
- ProgramsPage: optional ?type= filter (useSearchParams) so the spaces are
  real category links; M2 keeps its dedicated /m2-program page.
- ProgramDetailPage: fetch + render the program's Stadium project entries via
  getProjects({hackathonId: slug}) as a UnitCard grid linking to /m2-program/:id;
  falls back to the existing signup-derived project cards (PitchOff/Dogfooding).

Client-only; no server/schema/data changes.
feat(landing): program-type spaces entry point + per-program projects view
- helmet for X-Frame-Options / X-Content-Type-Options / etc. CSP disabled
  (JSON API, no HTML) and crossOriginResourcePolicy set to 'cross-origin'
  so the separately-hosted SPA can still read responses.
- express-rate-limit: app-wide 200/min/IP default + a tight 10/min/IP on
  the unauthenticated, signature-verifying /api/admin/session endpoint.
- trust proxy = 1 so client IPs are correct behind Railway's proxy.

helmet + express-rate-limit added to deps.
Lets someone without a Stadium project apply to a program. A public form
emails the team; they approve manually and reply (matching the original ask).

- POST /api/programs/:slug/applications/non-member (public, no auth): validates
  {name, email, walletAddress?, pitch}, honeypot field, then emails the team.
- non-member-application.service: sends ONE email to info@joinwebzero.com with
  sacha@joinwebzero.com cc'd and applicant details in the body. Fixed recipients
  only (applicant address never used as a recipient) so it can't be abused as an
  open relay. HTML-escaped body. Returns 503 if Resend isn't configured.
- email-transport: forward cc to Resend.
- Client: NonMemberApplyModal + a 'Don't have a Stadium project yet?' CTA on
  open program detail pages; api.submitNonMemberApplication.
- Tests: validation + the email to/cc/body + HTML-escaping.

Note: the original confirmation-to-applicant was intentionally dropped — it'd
let the endpoint email arbitrary addresses from our verified domain.
security: helmet headers + express-rate-limit (#127, #128)
feat(programs): non-member apply flow — emails the team (#140)
…ng artwork

The audio used to restart on every page change because each page renders its
own <Navigation> (which held the brightness-rack + the iframe), so it remounted.

- Move the SoundCloud iframe + widget + state into a SoundCloudAudioProvider
  mounted ABOVE the router in App.tsx, so playback persists across client-side
  navigation. brightness-rack reads state via useSoundCloudAudio() context.
- Playback view is now a 'now playing' card: track artwork (artwork_url, falling
  back to the profile avatar; upscaled to 200px) + title + 'pommeshdrms · genre'
  with discreet SoundCloud / Instagram links underneath + the mute toggle.
- Context/hook split into use-sound-cloud-audio.ts (react-refresh clean).
…ount-linking

Adds Google/Apple/passkey sign-in (via Supabase Auth) alongside wallet SIWS,
keeping the wallet as the authorization principal via account-linking.

- Migration auth_identity_links (supabase_user_id ↔ wallet).
- supabaseUser.js: verify a Supabase access token via supabase.auth.getUser
  (no local JWT secret needed); x-supabase-token header.
- identity-link repo + service (optionally mirrors email into wallet_contacts).
- auth.controller + /api/auth routes:
  - POST /link-wallet: requires a valid Supabase session AND a SIWS signature,
    records the link, returns the existing HMAC session bearer for the wallet.
  - POST /session-from-social: Supabase session → linked wallet → session bearer
    (409 needsLink if not linked). Reuses issueSessionToken so ALL existing
    route middleware authorize unchanged.
- CORS allows x-supabase-token. 7 new tests; full suite green (271).
Add a nullable `content` JSONB column to programs holding an ordered list
of typed sections (text / steps / schedule / lineup / stats / feedback /
cta) rendered as on-brand panels on the program detail page. Reusable for
every program without a per-section schema change.

- supabase migration adds `content` (additive, idempotent)
- program.repository maps content both ways
- validateProgramContent enforces typed sections, size caps, http(s) URLs
- ProgramContent renderer reuses LCDStat for stat tiles
- Dogfooding Denver fixture populated as a worked template, incl. Typeform
  feedback highlights + stats snapshot
- admin UI editing of content logged to the improvement backlog (deferred)
RESEND_API_KEY / RESEND_FROM_EMAIL power all outbound email through one
transport: project notifications (m2 approved / changes requested,
application accepted / rejected) and non-member program applications.
The old comment said it was "unused"; document that an unset key means
no email goes out and that RESEND_FROM_EMAIL must be a verified domain.
Add previous/next track buttons and a seek slider to the AUDIO row,
wired to the SoundCloud Widget API (next/prev/seekTo, PLAY_PROGRESS for
the live position, getDuration for length). Shows elapsed/total time and
a "FEATURING ARTISTS WE LOVE" caption to frame the music as curated.
fix(audio): persist SoundCloud playback across navigation + now-playing artwork
feat(auth): social sign-in server core — Supabase + wallet account-linking (part 1)
feat(programs): templatable rich content sections per program
docs(env): correct stale Resend comment — email transport is live
The merge of develop (which moved the SC widget into a provider above the
router) auto-combined with this branch's in-component transport edits and
produced a broken brightness-rack (duplicate `muted`, unterminated region)
that failed the build.

Re-implement prev/next + seek on the new architecture instead:
- provider owns positionMs/durationMs + next/prev/seek (PLAY_PROGRESS +
  getDuration), exposed via useSoundCloudAudio()
- brightness-rack renders the seek slider + prev/next + elapsed/total time
  and the "FEATURING ARTISTS WE LOVE" caption under the now-playing card
The SoundCloud profile loads a single long DJ set, so next/prev-track had
nowhere to go. Repurpose the two buttons to skip ±15s within the current
track via seekTo (reliable), with rewind/forward icons. Drop the unused
next/prev from the audio provider + context.
Lead with WebZero as the room where people build something cool, and call
out that each program is tied to a specific event with prizes (cash, event
tickets, merch) — clearer framing than the old "ship then keep going" line.
feat(brightness-rack): audio transport — prev/next + seek slider
copy(programs): reframe WHAT WE DO around events + prizes
Replace em-dashes (—) with commas, periods, colons, or parentheses across
user-facing copy: JSX text, toasts, labels, placeholders, aria-labels, and
WebZero-authored content (program descriptions + the Denver content). House
style: no em-dashes in our own copy.

Left untouched: code/CSS comments, builder-written project descriptions
(mockWinners, PitchOff submissions), numeric en-dash ranges, and the bare
"—" empty-value placeholder glyph in admin tables.
Lets an admin grant program access by email (no wallet). Foundation for
social sign-in onboarding (PR 2 adds the client magic-link UI).

- migration: program_admin_emails (program_id, email, invited_by); email
  stored lowercased so the PK is case-insensitive
- repository: list/add/remove/isAdminByEmail
- middleware: requireProgramViewer — a valid Supabase token whose email is
  a program admin gets VIEW access; otherwise defers to the wallet
  requireProgramAdmin. Applied to read routes only; mutations stay
  wallet/global-gated, so email admins are view-only
- controller + routes: POST /:slug/admins/invite, GET /:slug/admins/emails,
  DELETE /:slug/admins/emails/:email
- onboarding email: program-admin-invite template + service (Resend),
  best-effort send so a missing key never loses the grant
- tests: requireProgramViewer (valid email passes view-only, wrong email /
  bad token / no token fall back to wallet and are denied)
- env: document FRONTEND_URL for email links
Completes the email-admin onboarding (client side of the backend in the
previous commit).

- supabase client (null when VITE_SUPABASE_* unset) + useSocialAuth hook
  (email magic link via signInWithOtp; exposes token/email/authHeader)
- api: listProgramAdminEmails / inviteProgramAdminEmail / removeProgramAdminEmail
- ProgramAdminsSection: invite-by-email form + email-admin list (global
  admins), with emailSent feedback
- AdminProgramPage: three-way gate — wallet admin (unchanged) / social
  view-only viewer / sign-in panel (connect wallet OR email magic link).
  After social sign-in, probes authorization and renders applications
  read-only; ApplicationCard gains a readOnly prop
- env: document VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY
feat(admin): add program admins by email + onboarding sign-in (full feature)
copy: remove em-dashes from first-party app copy
sacha-l added 14 commits May 22, 2026 18:58
The unauthenticated POST /:slug/applications/non-member route emails the
team on every call and was only under the generous global 200/min limiter,
so a single IP could flood the inbox. Add a tight per-IP limiter
(5 per 15 min) on this route only. It can't relay to arbitrary addresses
(fixed recipients), so this just stops inbox flooding.
#158 shipped email magic-link admin sign-in via a social-token-direct +
view-only middleware design, making the #152 wallet account-linking bridge
dead code — it had zero client consumers.

Removed:
- /api/auth routes + auth.controller (link-wallet, session-from-social)
- identity-link service + repository
- auth_identity_links migration (never wired to a live consumer; if it was
  ever applied to a Supabase env, drop the table manually — nothing reads it)
- the obsolete auth.controller test

Kept (reused by #158): api/auth/supabaseUser.js and the x-supabase-token
CORS header.

Server tests: 284 pass (was 291; -7 for the deleted controller's tests).
Client build + lint clean (no client changes).
- Logo: the |||  icon (favicon) now sits left of "STADIUM" in both the top-nav
  brand and the landing hero headline.
- Audio: bind the SoundCloud FINISH event to restart playback so the music
  loops forever (it already survives navigation — the iframe lives above the
  router).
- Footer: extract a shared <SiteFooter> (kills the copy that had drifted between
  Layout and ProjectDetailsPage). New credit line "Created with ❤️ by
  sachalansky from WebZero" (sachalansky → x.com/sachalansky, WebZero →
  joinwebzero.com), app mono/LCD typography, and a SOURCE link to the Stadium
  repo alongside the existing org GitHub + X.
fix(security): rate-limit the public non-member apply route
…k-bridge

chore(auth): remove unused social account-linking bridge (#152)
feat(landing): logo by STADIUM + looping audio + unified footer
…e ruler

- Project details: convert the Overview/Milestones tab content (Final
  Deliverables, Submission Status, placeholders) from shadcn Card + sans/muted
  type to the app's panel + mono/LCD aesthetic (label-hw headers, hardware link
  rows, text-body). Tab labels become mono/uppercase. Drop the now-unused Card
  import. Align TeamPaymentSection's leftover muted token to text-label-dim.
- Hero tagline: "Stuff people build here. Browse our programs, leave your mark."
- Brightness rack: lay the hour scale out by time of day (00h…20h left→right in
  order, instead of by solar brightness which made 8h/16h collide). Phase
  anchors (NIGHT/DAWN/NOON/DUSK) sit at their real clock times and the green
  "now" tick marks the current time on the ruler.
design: re-land round-2 polish onto develop (#162 was stranded)
Three release-hardening fixes:

- #129: validate ADMIN_SESSION_SECRET at boot via assertSessionSecret(), called
  in server.js startup, instead of lazily on the first admin sign-in.
- #130: requireAdmin and requireTeamMemberOrAdmin now set req.user.chain on
  every grant path (admin + team-member), so audit logging has the chain.
- #134: factor csvCell + formula-injection defense into a shared api/utils/csv.js
  (csvCell + csvRow); program-inbox CSV export now imports it so future exports
  inherit the same RFC-4180 quoting + injection guard.

Tests: csv util (formula injection + quoting), assertSessionSecret (missing /
short / ok), and middleware req.user.chain assertions. Full suite 293 pass.
hardening: boot secret validation + req.user.chain + shared csvCell util (#129, #130, #134)
First official release. CHANGELOG.md documents the v1.0.0 feature set grouped
by what you can do with Stadium (discover, build/participate, run an event,
trust/safety). Client package.json bumped 0.0.0 -> 1.0.0 to match the server.
Stadium is for builders broadly, not Polkadot-only; keep the accurate
multi-chain (Substrate/Ethereum/Solana) detail which shows the breadth.
chore(release): v1.0.0 — CHANGELOG + version bump
@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 6:09pm

@sacha-l sacha-l merged commit 7588ee4 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