Adds 2 new models for grand-finale logistics PR 2:
- Hotel: 1:1 with Program (one per edition); name + address + link + notes
- FlightDetail: 1:1 with AttendingMember; arrival + departure datetimes,
flight numbers, airports, admin status (PENDING/CONFIRMED), admin notes
Migration is purely additive: no DROP/ALTER COLUMN/RENAME. FKs point
FROM new tables TO existing tables (Program, AttendingMember) with
ON DELETE CASCADE only firing on parent deletion.
- New components/admin/grand-finale/finalist-slots-card: per-category
quota editor with confirmed/pending counts, dirty-tracking, save button.
Renders an empty editor for both Startup and Business Concept categories
even when no quota exists yet.
- New components/admin/grand-finale/waitlist-card: per-category ranked
waitlist display with status badges + manual-promote AlertDialog
(audit-logged via FINALIST_MANUAL_PROMOTE).
- Round detail page: embeds both cards conditionally when
roundType === 'LIVE_FINAL'.
- New finalist router queries: listQuotas, listCategoryCounts (groupBy
on category+status), listWaitlist (rank-ordered with project relation).
Smoke-tested: setting Startup quota to 3 persists to DB; UI renders
quota editor and waitlist card cleanly with empty state.
- /finalist/confirm/[token] under (public) route group
- Browser-local-time deadline + zone label + live countdown
- Default-selects up to defaultAttendeeCap team members
- Per-member "Needs visa?" toggle that surfaces only when selected
- Decline AlertDialog with optional reason textarea
- Distinct friendly states for invalid / expired / already-confirmed /
already-declined / superseded tokens (not generic errors)
- Smoke-tested end-to-end against live dev server: confirmation row
flipped to CONFIRMED, AttendingMember row created with correct visa flag
- expirePendingPastDeadline service: scans PENDING confirmations past
deadline, marks each EXPIRED + audit-logs, then promotes the next
waitlist entry per affected category (using each program's grand-final
round configJson for windowHours).
- /api/cron/finalist-confirmations: hourly cron entrypoint (CRON_SECRET
header gate), wraps the service.
- finalist.addToWaitlist: insert at a specific rank, shifting later
entries down (transactional).
- finalist.reorderWaitlist: rewrite a category's rank order in one go,
using a temp-rank trick to avoid unique-constraint conflicts mid-update.
- finalist.manualPromote: out-of-rank-order admin promote with audit log
(FINALIST_MANUAL_PROMOTE) + fresh confirmation email.
2 new tests. Suite at 14/14 for finalist-confirmation.
- finalist.getByToken: public lookup of a confirmation by signed token,
with all the data the public page needs (project, team members, current
state). Throws on expired/tampered tokens.
- finalist.confirm: validates team membership of every selected user,
checks against program.defaultAttendeeCap, atomically writes
status=CONFIRMED + AttendingMember rows in a transaction.
- finalist.decline: captures optional reason, then promotes the next
WAITING waitlist entry in the same category (no-op if waitlist empty).
Resolves the new windowHours from the LIVE_FINAL round configJson.
- promoteNextWaitlistEntry service: encapsulates the cascade (mark
PROMOTED, create fresh PENDING confirmation, send email).
- New service module createPendingConfirmation: writes a PENDING
FinalistConfirmation row with a signed token whose exp matches the
computed deadline.
- selectFinalists admin mutation: reads windowHours from the round's
configJson.confirmationWindowHours (default 24), validates category
match + quota, then creates one confirmation per selected project
and sends a notification email to the team lead. Email failures are
logged but never roll back the row creation.
- New email helpers: getFinalistConfirmationTemplate +
sendFinalistConfirmationEmail.
Adds 4 new models for grand-finale logistics PR 1:
- FinalistSlotQuota: per-category mutable quotas
- WaitlistEntry: ranked per-category waitlist
- FinalistConfirmation: token-gated confirmation lifecycle (PENDING /
CONFIRMED / DECLINED / EXPIRED / SUPERSEDED) with optional decline reason
- AttendingMember: who from each team is attending, with visa flag
Plus Program.defaultAttendeeCap (default 3) for the per-edition team
attendance cap.
Migration is purely additive: no DROP/ALTER COLUMN/RENAME on existing
schema. All FKs ON DELETE CASCADE only fire on parent deletion.
Adds a project-centric ops view for mentor management:
- New mentor.getMenteeActivity tRPC procedure aggregates every project
with wantsMentorship=true and derives a status (unassigned / assigned
/ active / stalled) from the latest message + file activity.
- /admin/mentors becomes a tabbed page: existing Mentor list +
new Mentees & Activity table with status pills, search, and a
per-row Assign/Open CTA linking to /admin/projects/[id]/mentor.
- Includes 2 unit tests covering classification + program scoping.
Also: ignore .remember/ (plugin scratch dir).
- Switcher trigger now shows the current view's icon + label with a
chevron (e.g. "Admin View ⌄") instead of the vague "Switch View".
Dropdown adds a header, marks the current view with a checkmark,
and lists each accessible alternative explicitly.
- Adds a "Mentors" entry to the admin sidebar between Juries and
Awards so the existing /admin/mentors page is reachable from nav.
formatEnumLabel was leaving inputs uppercase ("TECHNOLOGY_INNOVATION"
became "TECHNOLOGY INNOVATION"); lowercasing first yields proper
title case ("Technology Innovation") and improves labels app-wide.
Apply it on the project mentor page for Ocean Issue + Category.
mentor.getRecentMessages: last N unread messages from teams across all
of a mentor's assignments. Drives a Recent Messages card on /mentor.
applicant.getMentorConversationPreview: last 3 messages + unread count
for a given project. Drives a 'Conversation with [Mentor]' card on
/applicant — auto-hides when no mentor is assigned.
Both procedures use the existing MentorMessage(projectId, createdAt)
composite index — no new index needed.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
Extract ROLE_SWITCH_OPTIONS + switchableRoles computation from the two
duplicated copies (role-nav.tsx + admin-sidebar.tsx) into a single
src/components/layouts/role-switcher.tsx module.
Adds a RoleSwitcherPill component placed top-right of every dashboard:
- Hidden for single-role users
- Hidden during impersonation
- Same visual + click target across /jury, /mentor, /applicant,
/observer, /award-master AND /admin (admin layout gains a small
top-bar to host the pill)
Removes the duplicate role-switcher items from the admin sidebar's
bottom user-menu — one source of truth instead of three.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
Banner wrapper now uses pointer-events-none so it doesn't intercept clicks
on the user-menu dropdown sitting underneath; the 'Return to Admin' button
re-enables pointer events on itself only.
Banner also lists every role the impersonated user holds (e.g.
'JURY MEMBER, MENTOR') instead of just the primary role, matching how
multi-role users are surfaced everywhere else.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
user.bulkUpdateRoles({userIds, addRole?, removeRole?}) batches role
changes across up to 200 users with a SUPER_ADMIN self-demote guard.
When MENTOR is freshly added, fires sendMentorOnboardingEmail once per
user, gated by User.mentorOnboardingSentAt for idempotency. Audit log
entry per user changed.
UI: 'Add MENTOR role' button surfaces in the existing /admin/members
bulk-selection toolbar when ≥1 user is selected. Other roles
(OBSERVER / AWARD_MASTER) supported by the procedure but not yet wired
to UI; one button keeps the toolbar minimal until a clear need arises.
Tests cover happy path, idempotency on second call, removeRole semantics,
and the SUPER_ADMIN self-demote guard.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
user.getDefaultDashboard returns the highest-priority role for which the
user has actionable work right now — pending eval in active round, active
mentoring assignment, applicant project in active round, etc. — falling
back to static priority order if nothing is actionable.
src/app/page.tsx now reads roles[] (multi-role array) instead of just the
primary role, fixing the bug where mentor+juror users always landed on
their primary role's dashboard. Uses static priority for simplicity in
the server component; the context-aware procedure remains available for
client surfaces.
Tests cover six cases: super-admin, juror with active eval, juror+observer
fallback, mentor+juror in mentoring round, both-active-priority-tiebreak,
observer-only.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
One-shot email sent when a user is first granted the MENTOR role.
Subject: 'Welcome to MOPC mentoring'. Includes a CTA to /mentor and
a hint about the Switch View pill for multi-role users.
Idempotency lives at the call site (User.mentorOnboardingSentAt
checked in user.bulkUpdateRoles / user.updateRoles).
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
Single nullable DateTime column. No backfill. Catalog-only ALTER TABLE —
sub-millisecond on PostgreSQL regardless of table size. The column is
unused until the bulk role-update flow wires it up as an idempotency
stamp for the mentor-onboarding email.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §D
mentor.getCandidates and mentor.getMentorPool both filtered on
status='ACTIVE', which excluded every seeded mentor (status=NONE)
and any INVITED mentor. Production scenario: PR 4's Manual Picker and
PR 5's pool counts both rendered empty against real data.
Filter changed to status != SUSPENDED — admins want to see all mentors
they manage (including INVITED + NONE), but not suspended ones.
Found via Playwright smoke of PR 5: pool count read 0 against 4 seeded
mentors with roles[]=['MENTOR'], status='NONE'.
Replaces the redirect-to-/admin/members stub with a sortable, searchable
list of all MENTOR-role users powered by mentor.getMentorPool. Columns:
name, expertise tags, country, active count, completed count, capacity
remaining, last activity. Header summary cards show pool size, total
active assignments, and average load.
Row links continue to /admin/members/[id]; /admin/mentors/[id] remains
a redirect (mentor-detail view deferred to a future PR).
Plan: docs/superpowers/plans/2026-04-28-pr5-mentor-round-overview.md
Test runs that crash before reaching afterAll leave orphan @test.local
users + programs (Test Program / getCandidates- / bulk- / source-flag-
/ mentor-files- name patterns). Mirrors tests/helpers.ts cleanupTestData
cascade order. Idempotent — safe to re-run any time the dev DB picks up
test pollution.
Run: npx tsx scripts/cleanup-test-pollution.ts
Replaces single-section AI-only stub with three sections (Project Context,
Currently Assigned, Pick a Mentor). Pick a Mentor is a tab strip:
- Manual Picker (default): all MENTOR-role users sorted by expertise
overlap %, with search + load/capacity columns. Assign sends
method=MANUAL.
- AI Suggestions: existing pane, with an amber 'AI matching unavailable'
banner + 'Tag overlap' pills when OPENAI_API_KEY is unset.
Plan: docs/superpowers/plans/2026-04-28-pr4-mentor-assignment-ux.md
Pure function reused by upcoming mentor.getCandidates + AI fallback path.
Refactors getAlgorithmicMatches to call it. No behavior change.
Plan: docs/superpowers/plans/2026-04-28-pr4-mentor-assignment-ux.md
Adds a PROJECT_TEAM recipient type to the message router (resolver
returns team members + project lead) and an "Email Team" button on
the admin project detail page that opens a self-contained dialog
matching the look of /admin/messages: subject, body (pre-filled
with "Hello [Project Title] team,\n\n"), live HTML preview iframe,
"Send test to me" + "Send to N" actions.
The composer reuses the existing message.previewEmail and
message.send tRPC procedures end-to-end — no parallel email
infrastructure introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces every MentoringConfigSchema field on the round Config tab:
- Adds "Mentoring Request Window" card with mentoringRequestDeadlineDays
numeric input (1-90 days, default 14) and passThroughIfNoRequest toggle
(default ON; OFF holds projects PENDING until manual mentor assignment).
- Adds inline help-text for the Eligibility dropdown explaining each
option's effect on auto-PASS behavior.
- Hides the General Settings card on MENTORING rounds (it only renders
Advancement Targets, which don't apply to a pass-through round).
- Relaxes the Launch Readiness "File requirements set" gate for MENTORING
rounds without filePromotionEnabled + a target window — file requirements
only matter when files will be promoted to a downstream submission window.
Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §A
Plan: docs/superpowers/plans/2026-04-28-pr3-mentoring-config-completeness.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds generateMentorObjectKey helper producing
<projectName>/mentorship/<timestamp>-<file>. Replaces the
client-supplied bucket/objectKey on workspaceUploadFile with an
HMAC-signed upload token that binds bucket, objectKey, uploader,
and a 1h expiry — paths can no longer be forged from the client.
Adds workspaceGetUploadUrl, workspaceGetFiles,
workspaceGetFileDownloadUrl, workspaceDeleteFile procedures with
mentor-or-team-member auth. Builds <WorkspaceFilesPanel> and
wires it into the mentor workspace Files tab and the applicant
/applicant/mentor page. Replaces the file-promotion-panel mock
array with a real workspaceGetFiles query.
Tests cover token sign/verify (5), key construction (5), and
end-to-end procedure flow including auth + tampered tokens (7).
Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §F.1
Plan: docs/superpowers/plans/2026-04-28-pr2-mentor-workspace-files.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle backend security (HMAC-signed upload tokens, server-built
objectKeys, mentor-or-team-member auth) with the actual file UI
that didn't exist yet (Files tab placeholder, file-promotion-panel
mock array, and applicant-side gap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Confirm Your Evaluation Preferences" banner was including jury
group memberships whose only rounds are LIVE_FINAL or DELIBERATION.
Those ceremonies don't use cap+category preferences, so the sliders
were meaningless. Filter getOnboardingContext to memberships in
groups with at least one INTAKE/FILTERING/EVALUATION/SUBMISSION/
MENTORING round.
Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §E
Plan: docs/superpowers/plans/2026-04-28-pr1-jury-preferences-filter.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step-by-step plan for §E. Single-procedure change to filter
getOnboardingContext memberships by linked-round type, plus a
new test file covering review-only, LIVE_FINAL-only, and mixed
group cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After review, two additions to the multi-role UX section:
1. Replace static-priority post-login redirect with context-aware
"go where the work is" via new user.getDefaultDashboard() — a
juror+observer landing during an active jury round goes to /jury
even though observer has no work; falls back to static priority
when no role has actionable work.
2. Standardize the role switcher's location across all dashboards.
Extract shared useRoleSwitcher hook + new RoleSwitcherPill that
renders in the top-right of every layout, including admin (which
currently puts switching in the bottom-left sidebar pill).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Balance-juror-grading and factor-advance-votes switches now live as a
compact card above the per-category sections on the rankings page,
always visible. Hiding them inside the project modal was confusing
because flipping them re-sorts the entire list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Reset to system order' button per category (Startups and
Business Concepts). The button only appears when admins have
drag-reordered that category — otherwise there's nothing to reset.
Clicking it wipes that category's reorder history from the snapshot's
reordersJson via a new ranking.clearReorders mutation, after which
the dashboard re-initializes and falls back to the live composite
ranking (balanced/raw score, optionally blended with balanced pass
rate per the toggles).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toggle was previously a balanced-vs-raw switch for the pass-rate
input, but pass rate was always present in the composite formula.
That contradicted the original ask — admins want a clean way to say
'don't factor in yes/no at all'. Toggle off now drops pass rate from
the composite entirely; the ranking falls back to pure
balanced-or-raw score, matching the behavior before this thread of
work introduced the composite formula.
The label is updated accordingly ('Factor advance votes into ranking'),
and the pass-rate chip on each list row only appears when the toggle
is on so admins aren't shown a number that isn't influencing rank.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three side-panel additions on the ranking dashboard's project detail
sheet:
- Project country and team name as outline badges in the header,
matching the row chips on the list view.
- Collapsible 'Description' box (closed by default) that reveals the
full project description without leaving the panel.
- Expanding a juror row now shows their per-criterion scores in
addition to the free-text feedback. Boolean criteria render with
the form's trueLabel/falseLabel (or 'Yes'/'No' fallback); numeric
criteria show the raw value next to the criterion label from the
active form's criteriaJson.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dashboard now computes its own composite ranking score on the
client, blending (balanced-or-raw) average score with (balanced-or-raw)
advance pass rate via the existing scoreWeight / passRateWeight
sliders. Both inputs are toggled independently:
- 'Balance juror grading style (score)' — existing useBalancedRanking
- 'Balance juror approval rate (advance vote)' — new useBalancedPassRate
Both default to true and persist per-round. The pass rate is balanced
the same way scores are: each juror's personal yes-rate gives them a
Bernoulli stddev, each vote is z-normalized against that, and the
project's mean z is rescaled to the round's overall yes rate. A 'yes'
from a juror who rarely says yes counts more than a 'yes' from a
lenient juror.
List rows now show two chips — score (Bal/Raw X.XX) and pass rate
(Bal Yes% / Yes% N%) — so admins can see what's driving the order.
The threshold cutoff and live re-sort effect both use the same
composite formula.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The side panel only read Evaluation.binaryDecision, which is null when
the round's evaluation form stores the 'proceed to next round' answer
as a boolean criterion in criterionScoresJson (the current round's
shape). project.getFullDetail now resolves a unified `decision` field
per assignment using the same fallback pattern as the ranking router:
prefer the column, fall back to a type='advance' (or legacy 'move to
the next stage' boolean) criterion looked up by id in the active form.
Also: project country in the rankings list now renders whenever it's
present, not only when teamName is also set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ranking dashboard now refetches roundEvaluationScores every 30
seconds. When a juror submits a new evaluation, the next refetch
updates the raw and balanced averages, and the existing re-sort
effect (now also keyed on snapshot + evalScores, not just the toggle)
re-orders the list in place. Manual reorders persisted on the
snapshot still take precedence — admins who have dragged rows aren't
overruled by score updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each ranking row now displays a small chip with the score that's
currently driving the rank — the juror-balanced average when the
round's useBalancedRanking toggle is on, the raw juror average when
it's off. The chip is labeled 'Bal' or 'Raw' so the source is
unambiguous. Per-juror score pills stay alongside; full Raw +
Balanced detail still lives in the side panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Project-level averages (Raw + Balanced in the side panel, observer
project detail score card, observer preview dialog Avg Score) now
show two decimals (e.g. 8.33 instead of 8.0/8.3) so admins can see
the actual computed value. Per-juror individual scores keep one
decimal — they're submissions, not aggregates. ScorePill gains an
optional precision prop so call sites can opt into 2-decimal display
where the value is an aggregate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the 'How scores are calculated' affordance to:
- the admin ranking dashboard side panel (next to the Avg Score card)
- the observer full project detail page (in the score card)
- the observer reports preview dialog (next to Evaluation Summary)
so all three audiences can open the same explainer dialog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reusable component used by admin and observer surfaces. Covers the
algorithm, a five-step plain-language walkthrough, a worked example
with three jurors of different grading styles, edge cases, and why
both Raw and Balanced are always shown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the ranking router's roundEvaluationScores response with
per-juror grading stats (mean, stddev, count) plus the round's overall
mean/stddev. The side-sheet juror rows render 'typical X.XX →
contributes Y.YY' next to each Score badge whenever balanced is on,
making the z-rescaling visible per individual rather than only as a
project-level number.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the per-row '⇢ X.X' annotation from the ranking list — the
list view stays clean. The side panel's stats area gains a combined
Avg Score card that shows Raw and Balanced side-by-side, with the
active one (per the round's toggle) bolded and tagged 'used for
ranking'. Pass Rate and Evaluators move below into a 2-col grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two existing sort sites (initial init + threshold cutoff) now read
from the local toggle. A second effect re-sorts the list when the
toggle flips, but only when no manual reorder is pinned to the
snapshot — persisted manual reorders always win, matching prior
behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>