You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Active Vue pages: src/views/admin/AdminSystemDesignView.vue, src/views/EventView.vue, src/views/ArchiveEventView.vue
Storage: event.schedule rows with type system_design, optional public recap notes, and prompt-link resources
Draft generation: POST /api/events/[eventId]/system-design/draft reads public Google Slides prompt decks and returns extracted title, raw content, and a generated summary
Attendance analysis
Active Vue pages: src/views/admin/AdminAttendanceOverviewView.vue, src/views/admin/AdminAttendanceView.vue
CSV parser and summary metrics: lib/luma-attendance.ts
Per-Module Breakdown
Mock DB (lib/mock-db/)
Entry point:lib/mock-db/index.ts
Key functions:
readData<T>(filename) — reads data/{filename}.json, returns [] only when the file is missing, and throws on invalid/non-array JSON.
writeData<T>(filename, data) — serializes writes via a per-filename promise queue and replaces files through temp-file write + rename.
Non-obvious logic: The write queue chains promises per file key — concurrent writes to events and sessions can overlap, but concurrent writes to the same file are serialized inside one process. Atomic rename reduces partial-write corruption, but JSON files are still not a multi-process production store.
Each entity module (events.ts, talks.ts, etc.) exports typed helpers like getAll*, get*ById, create*, update*.
event-checklists.ts creates a default chronological run sheet on first read. Monthly events use the full CFP/program/post-event checklist, while quarterly meetups use the short setup checklist for creating the event shell and adding the G-Meet link. For existing events, it infers already-reached milestones from the current event status so completed events start with post-event tasks instead of a blank checklist. Event-specific disabled milestones stay visible but do not count toward progress or status backfill.
Supabase (lib/supabase/, supabase/)
lib/supabase/browser.ts exports a browser client using VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY; auth session persistence is disabled because tester feedback does not use Supabase Auth.
lib/supabase/server.ts exports a server/admin client using VITE_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY; keep the service key server-only.
vite.config.ts loads .env.local and injects it into the Hono dev server so local server routes can read Supabase secrets.
types/supabase.ts currently types the feedback tables by hand until generated Supabase types are introduced.
supabase/migrations/20260530000000_feedback.sql creates feedback_testers for the original testing loop and feedback_submissions for route-level and event-level feedback rows.
supabase/migrations/20260617001000_event_feedback_response_tokens.sql adds hashed anonymous response tokens so event feedback can reject repeat submissions from the same browser/event pair.
supabase/migrations/20260615000000_community_events.sql adds community_events, modeled from the current devcongress.org Astro meetup collection and seeded with the existing website meetup YAML entries.
supabase/migrations/20260620032000_event_series_type.sql adds an explicit series_type field (monthly, quarterly, or special) so organizers stop relying on event titles to drive attendance and organizer workflow decisions.
supabase/migrations/20260615001000_meetup_media_bucket.sql adds the public meetup-media Supabase Storage bucket for selected image uploads.
Row-level security allows public reads of active tester names and public inserts of new feedback, but not public reads of submitted feedback; organizer reads and status updates go through authenticated server routes using the service role.
Row-level security allows public reads of community_events only when publish_to_website = true; trusted organizer writes use the server-side service role.
/api/health/supabase verifies that server-side Supabase config is present and that the feedback tester table is reachable.
/api/health/supabase/community-events verifies that the Supabase event-source table is reachable.
/api/health/supabase/storage verifies that the meetup-media bucket is reachable.
/api/events reads, writes, and removes Supabase community_events when APP_DATA_SOURCE=supabase is set with credentials, falling back to JSON events when Supabase is unavailable or the table has not been migrated. Local/dev runs default to JSON even if Supabase credentials exist in .env.local.
/api/integrations/luma/preview is the organizer-only read-only Luma step. It returns the event shell and duplicate-import state without creating a community_events row.
/api/integrations/luma/public-preview turns the current Luma draft into the same public meetup DTO used by /events/:slug, so organizers can inspect the website-style event shell before importing even when schedule, speakers, and gallery details are still empty.
/api/integrations/luma/import imports from a public Luma event URL only after organizer confirmation, requires Supabase-backed community_events, lets the organizer choose whether the event is monthly, quarterly, or special, stores source metadata to prevent duplicate imports when the Luma metadata migration is present, and keeps the new event unpublished in organizer draft mode until the organizer explicitly publishes it. Before those migrations are applied, import falls back to registration-URL dedupe and saves the event without unsupported columns.
Hono Server (server/)
Entry points:
server/app.ts exports the fetch-compatible Hono app used by Vite dev server.
server/index.ts starts Bun in production, serving /api/* through Hono and all other paths from dist/.
/api/events, including admin-only checklist and attendance analysis/import/removal routes under /api/events/[eventId]/checklist* and /api/events/[eventId]/attendance*
/api/integrations/luma/import
/api/talks
/api/leaderboard
Public website contract:/api/public/meetups* reads Supabase community_events first only when the server Supabase runtime is enabled, then falls back to current Event + published Talk JSON data. It returns a DevCongress.org-friendly meetup DTO with slug, series_type, start, end, cover, location, speakers, schedule, photos, counts, and app route URLs. System-design recap text and prompt-deck links travel inside the meetup schedule rows. Quarterly meetup recaps can also carry raw shared_links URLs in schedule rows, and public pages render those links only for quarterly meetups. /api/public/archive* returns narrow public archive payloads with event recap metadata, series type, cover, photos, feedback availability, shared links, and published talks so quarterly recaps can be media-first without showing an empty talk archive. /api/public/home provides the public landing aggregate without the broad /api/overview payload. These endpoints are read-only, CORS-enabled, and cacheable for short-lived website consumption; internal /api/* routes require organizer auth except auth and minimal attendee-feedback endpoints.
Public website verification:pnpm verify:public-api validates the public meetup response shape against the current devcongress.org Astro meetup schema expectations, plus CORS headers, cache headers, detail lookup, and talks lookup against PUBLIC_API_BASE_URL before the Astro website is wired to consume it.
Public events page:/events consumes /api/public/meetups inside this app so organizers can inspect the same public event stream the website will later consume.
Auth note: Hosted admin routes use Supabase Google OAuth plus app-owned HTTP-only sessions stored in admin_sessions; owner-only organizer email management lives at /organizer-console/organizers, and owner-only audit review lives at /organizer-console/audit-log. Local development falls back to ADMIN_PASSWORD unless APP_DATA_SOURCE=supabase enables the Supabase runtime.
Vue App (src/)
src/main.ts mounts Vue, Pinia, Vue Router, and the shared TanStack Query plugin.
src/router.ts lazy-loads routed page components with dynamic imports, keeping the shell and route guard eager while splitting public pages, organizer workspaces, quiz views, and fallback pages into route chunks.
src/App.vue provides the active shell/nav, contextual breadcrumbs for public and organizer routes, mounts AppToaster, polls /api/quiz/active so the public Play link appears only while a quiz session is waiting or active, reads the organizer Feedback Hub badge from the shared route-feedback inbox query cache, resolves event breadcrumb labels from cached event data or the single-event API instead of fetching the full event list on every route change, renders CFP and one-time speaker intake links as standalone public forms without app chrome, replaces full organizer workspaces with the mobile ops view on phone-width routes except login/auth and feedback QR display, and redirects organizer routes back to login if the cached/admin-session query later resolves unauthenticated.
src/App.vue renders src/components/AdminEventTabs.vue once for event-scoped organizer routes, keeping sub-section tabs stable while routed event pages change underneath.
src/components/ui/AppToaster.vue wraps vue-sonner with the DevCongress editorial/ops toast theme; app code should call notify from src/lib/notify.ts instead of importing toast directly.
src/components/ui/ViewSkeleton.vue provides reusable skeleton variants for full-page loading states; prefer it over bare loading text so routed views preserve their header, panel, table, and form structure while data fetches.
src/components/FeedbackBot.vue mounts globally on public routes only; it captures typed or anonymous route feedback and inserts feedback_submissions with trigger_source = route_feedback, page path, user agent, and viewport context. On small screens, the launcher routes to src/views/RouteFeedbackView.vue instead of opening an overlay.
src/views/FeedbackView.vue renders an event-scoped campaign from a minimal attendee-safe /api/feedback/events/:eventId payload; campaigns are open when manually set to active, or when draft with auto-open enabled and the event status is completed. The default auto-open response window starts at the event date and closes 3 days later unless an explicit campaign close time is set. Public event feedback also sends a per-event anonymous browser token so the server can reject duplicate submissions without requiring attendee email.
src/views/ArchiveEventView.vue reads /api/public/archive/:eventId, shows uploaded recap photos and quarterly-only raw links shared during the meetup, hides the empty talk archive for talkless quarterly meetups, and shows the community “Give Feedback” CTA only while that public archive payload says the form is open.
src/views/DashboardView.vue renders the community hub from /api/public/home, using only public counts, recent published talk cards, and an optional public CFP event pointer instead of the broad organizer overview aggregate.
src/views/EventsView.vue now mirrors the DevCongress website meetup listing shape, reads /api/public/meetups through the shared TanStack Query cache, and sends past-meetup View recap CTAs straight into src/views/ArchiveEventView.vue while upcoming/live cards stay in the public meetup flow.
src/views/EventView.vue consumes /api/public/meetups/:slug to render the meetup cover, schedule, speakers, photos, and status CTA; system-design rows point into the meetup archive page, and the public meetup page itself no longer repeats the archive recap block inline.
src/views/ArchiveView.vue filters completed events by year, query, topic, and speaker from /api/public/archive; archive search and speaker filters also look at saved schedule metadata so system-design recap text and facilitators still help people find the right meetup.
Shared event reads now normalize stale lifecycle states from the saved dates before the client sees them, so a published meetup that has already ended resolves to completed across the archive, dashboard, attendance, and feedback flows even if the stored row was left on live or another pre-event status.
src/views/NotFoundView.vue is mounted by the final Vue Router catch-all route for unknown client paths.
src/views/admin/AdminMobileOrganizerView.vue is the phone-only organizer surface. It shows current/next event cards, public/registration/source links, and feedback QR access when feedback is open, while full setup, editing, reporting, audit, and bulk table work stays tablet/laptop-only.
src/views/admin/AdminAttendanceOverviewView.vue renders the monthly attendance ledger, import coverage, source-quality readout, repeat-attendee count, and venue-planning summary from /api/attendance/monthly.
src/views/admin/AdminFeedbackOverviewView.vue renders the Feedback Hub as two quiet entry cards: website feedback from the shared /api/feedback/inbox query, and event feedback reports grouped by selectable period. Detailed stream content stays hidden until an organizer opens one card, the website inbox only fetches after a manual load/refresh action, event reports default to the current month when that period exists, event rows now keep a lighter identity block on the left with a full-width stat strip plus end-aligned action on desktop, and website feedback uses optimistic mutation plus query invalidation when organizers move notes through new, reviewing, done, and wont_fix.
src/views/admin/AdminEventsView.vue reads the organizer event list through the shared TanStack query layer, shows focusable detail popovers for the compact lifecycle legend, can upload a picked cover image during event creation, imports existing Luma events into Supabase from the create-event page, and invalidates the event-list plus overview queries after creating a new event.
src/views/admin/AdminAuditLogView.vue reads audit rows through the shared TanStack query layer and now swaps directly into a dedicated audit-log skeleton while the route data is loading.
src/views/admin/AdminEventView.vue invalidates shared event/overview queries after checklist, program-outline, photo-link, and media-upload mutations so status, schedule, and media changes stay visible across routes.
src/lib/meetup-media-client.ts centralizes browser-side meetup image validation, compression, and upload helpers so organizer create/edit surfaces share the same storage limits and encoding behavior.
src/lib/event-form.ts centralizes Zod validation for organizer event creation so the create form and /api/events server endpoint share the same required-field, slug, date, and URL rules.
src/views/admin/AdminAttendanceView.vue uploads/replaces a Luma CSV and renders post-event import metrics, source/ticket breakdowns, checked-in guests, and approved no-shows.
src/views/admin/AdminEventView.vue renders the shared chronological event checklist from /api/events/:eventId/checklist; checking status milestones can advance the event state, while the status dropdown remains available for manual correction. Unpublished events can disable incomplete checklist milestones that do not apply to that event.
src/views/admin/AdminEventView.vue also manages optional program outlines in event.schedule, letting organizers add structured time/title/type/lead/description/resource rows when a meetup has a run of show. A separate quarterly-only Shared links panel lets organizers paste raw recap URLs without titles; those links are saved into a dedicated schedule bucket and preserved when the outline is edited. The editor includes a monthly system-design scenario helper for Google Slides prompt decks, empty outlines are allowed, and event feedback can reuse saved schedule rows as activity prompts.
src/views/admin/AdminSystemDesignView.vue now calls /api/events/:eventId/system-design/draft when organizers click Generate Draft with a Google Slides prompt URL, fills the scenario title if it was blank, writes the returned summary into the full-width public recap field, and switches back to a saved/read-only state after persistence with explicit edit/remove actions for each saved scenario. When the event already has a matching system-design slot in the program outline, this editor now updates that existing row in place instead of appending a duplicate system_design row at the bottom.
src/views/admin/AdminEventView.vue also manages event media: organizers can upload selected cover/photo images to Supabase Storage or add website-compatible { url, type } links where type is image for direct media or folder for shared galleries.
src/views/admin/AdminQuizView.vue generates local QR-code join links for the live lobby.
Legacy Next pages/components remain in app/, components/, and hooks/ as a reference while routes are ported.
Quiz State API (server/quiz-state.ts, server/app.ts)
Advance command:POST /api/quiz/state/advance checks whether the current active question should transition from answering to revealing because time expired or all participants answered.
Non-obvious logic:GET /api/quiz/state is now read-only; polling clients call the explicit advance command before fetching state. This removes hidden mutation from GET while preserving polling behavior until a realtime/job-backed state machine exists.
correct_index is stripped from current_question in the state payload; player-specific reveal data is returned through player_result.correct_index after answering.
A SIMULATED_DELAY_MS (300ms) setTimeout is added to simulate realistic network latency.
JS-side mirror of the Tailwind theme in tailwind.config.ts — keep both in sync when rebranding
Current public theme direction is the devcongress.org light palette: cream page background, black ink text/borders, yellow brand fills, and pink primary accent. @fontsource/inter supplies the body/UI font; IBM Plex Mono remains the mono/accent font.
Configuration
Variable / Property
Default
Purpose
DEFAULT_TIME_LIMIT
20 (seconds)
Per-question time limit
DEFAULT_POINTS
1000
Base points per correct answer
POLL_INTERVAL_MS
1500
Quiz state polling frequency
SIMULATED_DELAY_MS
300
Fake network latency in API routes
REVEALING_DURATION_MS
5000
Time players see correct answer + distribution
SCOREBOARD_DURATION_MS
5000
Time players see leaderboard between questions
STREAK_BONUSES
{2:100, 3:200, 4:300, 5:500}
Bonus points per consecutive correct streak
VITE_SUPABASE_URL
unset
Supabase project URL for browser and server clients
VITE_SUPABASE_ANON_KEY
unset
Browser-safe Supabase anon key
SUPABASE_SERVICE_ROLE_KEY
unset
Server-only Supabase service role key for trusted admin operations
All constants are in lib/constants.ts.
Key API Flows
CFP Submission
POST /api/cfp
body: { event_id, speaker_name, speaker_email, github_username?, title, topic?, abstract?, bio? }
→ creates a speaker proposal for organizer selection
→ accepts submissions only for upcoming monthly events with CFP open
→ does not create a confirmed talk or require a prior speaker allowlist row
GET /api/events/[eventId]/speaker-submissions
→ organizer-only speaker proposal inbox for the event
PATCH /api/speaker-submissions/[submissionId]
body: { status: 'selected' | 'not_selected', internal_note?, expires_in_days? }
→ records the organizer decision
→ selecting generates a one-time selected-speaker confirmation link
Manual Talk Entry (Admin)
POST /api/events/[eventId]/talks
body: { speaker_name, speaker_email, title, topic?, abstract?, bio?, github_username?, slides_url?, publish? }
→ creates/keeps the speaker allowlist row for that email
→ creates an accepted talk when slides_url is empty
→ creates a slides_received talk when slides_url is present
→ creates a published archive talk when publish is true
Speaker Archive Intake
POST /api/events/[eventId]/speaker-intake-links
body: { expires_in_days }
→ admin-only; creates a month-scoped one-time token for the Legacy Backfill shelf
GET /api/events/[eventId]/speaker-intake/[token]
→ returns public event context only when the link is active
POST /api/events/[eventId]/speaker-intake/[token]
body for archive_backfill links: { speaker_name, speaker_email, title, topic?, abstract?, bio?, github_username?, slides_url? }
body for selected_speaker_confirmation links: { slides_url }
→ creates/keeps the speaker allowlist row for that email
→ archive-backfill links create accepted or slides_received talks from submitted details
→ selected-speaker links create slides_received talks from the original CFP proposal plus the submitted slides URL
→ marks the speaker link as used after a successful submission
→ expired or used links return closed-link errors
→ never publishes directly from the public form
DELETE /api/events/[eventId]/speaker-intake-links/[linkId]
→ admin-only; removes a generated speaker link from the Legacy Backfill shelf
Talk Status Lifecycle (Admin)
PATCH /api/talks/[talkId]
body: { status: 'accepted' | 'rejected' | 'slides_received' | 'published' }
→ requires admin cookie session when changing status
POST /api/talks/[talkId]/reminder
→ logs an organizer slide reminder for accepted talks without uploaded slides
Quiz Session Lifecycle (Admin)
POST /api/quiz/sessions → create session (status: 'draft')
PATCH /api/quiz/sessions/[id] → partial QuizSession field updates
Player Join + Play
POST /api/quiz/join body: { join_code, nickname, device_id }
→ creates/finds User by deviceId, creates QuizParticipant
→ increments User.events_participated when joining a new session
→ returns { session_id, user_id, participant_id }
POST /api/quiz/state/advance body: { session_id } explicit phase tick
GET /api/quiz/state?sessionId=&userId= read-only state fetch, polled every 1500ms
POST /api/quiz/answer body: { session_id, user_id, answer_index }
→ scores via scoring.ts, updates QuizParticipant totals and User.total_points
Quiz Question Management (Admin)
POST /api/quiz/questions body: { quiz_session_id, question_text, options[4], correct_index, order_index, time_limit_seconds?, points? }
PATCH /api/quiz/questions/[questionId] body: Partial<Question>
DELETE /api/quiz/questions/[questionId]
POST /api/quiz/questions/reorder body: { session_id, question_ids[] }
POST /api/quiz/sessions/[sessionId]/questions/from-paper
multipart/form-data: { file: PDF, question_count? }
→ requires admin session, extracts text locally, appends prototype rule-based questions to the session
Slides Upload Endpoints
POST /api/events/[eventId]/speaker-intake/[token]
body for selected_speaker_confirmation links: { slides_url }
→ tokenized selected-speaker links accept public slide links without an organizer session
→ selected-speaker links create slides_received talks from the original CFP proposal