- 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 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).
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
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
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'.
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>
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>
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>
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 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>
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>
The observer full project page used to call getProjectDetail without
a round, getting cross-round contaminated stats. It now resolves a
default — the currently OPEN round the project is in, falling back
to the most recently CLOSED one — and renders a selector chip in
the score card whenever the project participated in more than one
candidate round. Initial selection respects the ?round= query param.
A new observer procedure (getProjectRoundsForObserver) returns the
project's open or closed rounds for the picker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The admin dashboard fetches its side-sheet detail from project.getFullDetail
(not analytics.getProjectDetail as the audit assumed), and that procedure
had the same cross-round contamination bug. Add an optional roundId to
its input, filter the SUBMITTED-evaluations query when provided, and pass
roundId from the dashboard's useQuery so the Avg Score / Pass Rate /
Evaluators card now matches the per-juror list rendered below it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the edition-level branch of analytics.getProjectRankings
(programId mode) pooled every juror's evaluations across every round
into a single z-normalization context. A juror's mean and stddev are
not stable across round types — quick intake screening produces a
very different grading profile than a deep evaluation round, and
mixing them yields a meaningless personal calibration.
The rollup now groups points by roundId, computes one balance context
per round, and aggregates per-project as the unweighted mean of the
per-round balanced averages. roundId mode is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The procedure pulled every SUBMITTED evaluation for a project across
every round it ever participated in, then computed Avg Score / Pass
Rate / Evaluators from that pool. Meanwhile the per-juror list rendered
in the admin sheet filters to the current round, producing a card that
disagreed with the visible list. With roundId in the input, callers
opt into round-scoped stats; omitting it preserves the old aggregate
behavior for any caller that hasn't been updated yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a shared juror-balancing utility (z-score normalization per juror,
rescaled back onto the raw 1-10 scale) and wires it into:
- Admin reports page: Top-10 project table now shows "Raw Avg" and
"Balanced" columns side by side, and the summary stats row shows a
balanced-average tile. Sort defaults to balanced so harsh and lenient
graders no longer skew the ranking.
- Ranking dashboard: each project row shows a green/amber balanced-score
chip next to the raw average when the two differ by ≥0.05, making it
obvious when juror calibration moved a project's effective ranking.
Also adds AI Juror Calibration Advisory — a mutation that takes
anonymized per-juror stats, calls OpenAI, and produces a plain-language
explanation of the cohort's grading patterns plus per-juror severity
(normal / notable / outlier) with a one-sentence narrative. The advisory
describes the statistical balance that already runs; it does not
introduce a new weighting layer. Rendered as a panel in the Juror
Consistency tab when a specific round is selected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getProjectRankings and getAllProjects in the analytics router were
loading every assignment for each project regardless of the selected
round, so per-round rankings in the admin reports were counting
evaluations from prior rounds and mixing their scores into averages.
A Round 2 view with 3 reviews per project was showing 5 evaluations
and a skewed average that included Round 1 data.
Filter the nested assignments include by the round (or program) the
request is scoped to, matching the pattern already used by the ranking
service and by getStatusBreakdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ranking dashboard showed 0/X Yes for every project in rounds using
the 'advance' criterion type because the matcher only looked for
type === 'boolean' with a "move to the next stage" label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a juror cannot connect during an evaluation round, an admin
can now submit evaluations for them.
Router — new admin procedures:
- adminStart / adminAutosave: create and save drafts for any juror.
- adminSubmitOnBehalf: submit bypassing ROUND_ACTIVE and voting-window
checks. COI block and feedback/criterion validation still enforced.
Audit log records both admin and juror IDs plus bypassedWindow flag.
- getJurorAssignmentsForRound: list a juror's assignments + eval state.
UI — two new admin pages under /admin/rounds/[roundId]/jurors/[userId]/:
- evaluate: list of pending + completed assignments, COI flagged.
- evaluate/[projectId]: evaluation form reusing the juror's scoring UI,
with an "acting on behalf" banner and confirmation dialog before
submit. Back button returns to the assignments list.
Entry point: FilePen icon on each juror row in JuryProgressTable.
Refactor: extracted the scoring form JSX into shared
EvaluationFormFields component so the juror page and the admin proxy
page render identical inputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Jury dashboard now shows "Submitted" badge (green) with "Edit
Rankings" button when juror has already voted, instead of always
showing "Vote Now" — prevents confusion about whether vote saved
- Award-master page now shows project logos next to project names
- Backend getMyAwardDetailEnhanced now returns logo URLs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace custom download-only file list with full FileViewer component
that supports inline preview (PDF, video, images, Office docs),
open in new tab, and download
- Add project logos next to project names in award voting cards
- Backend now returns full file metadata (mimeType, size, pageCount,
language detection) and project logo URLs
- Award jurors can access files for eligible projects (access control
already added in prior commit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Jury dashboard now shows active award voting banners with project
count, deadline countdown, and direct link to vote
- Award voting page shows full project details: description, team
members, tags, and downloadable files in expandable cards
- Award jurors can now download files for eligible projects (added
awardJuror access check to file.getDownloadUrl)
- Backend query enhanced to include files and team members
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Email lookups used findUnique (case-sensitive on PostgreSQL) but user
input was lowercased, causing login failures for users with mixed-case
emails stored in the DB (e.g. Laurent_Faure@dietsmann.com). Also
normalized 7 affected emails to lowercase on the production DB.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files uploaded by admins with roundId but no requirementId were not
counted in the finalization page Docs column.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The admin upload flow accepted roundId but never wrote it to the
ProjectFile record, causing all admin-uploaded files to appear under
"General". Fixed the create call, the listByProject filter, and the
listByProjectForStage grouping to also use the direct roundId field.
Jury assignments on the project detail page are now grouped by round
with per-round completion counts instead of a flat list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project names now link to their detail page on all finalization tabs.
Submission/intake rounds show a docs submitted/required column.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an "Additional Roles" checkbox section below the primary Role
dropdown. Admins can now grant a user multiple dashboard views (e.g.,
Observer + Jury Member) without changing their primary role. The roles
array is saved alongside the primary role and used by the role switcher.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds bulkInviteMembers procedure to juryGroup router and integrates
BulkInviteForm into the jury group members tab. Also removes the
JURY_MEMBER-only filter from the user search — any user can now be
added to a jury group.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ability to define completely different evaluation criteria for each
competition category. Admins toggle "Separate Criteria per Category" in
round config, then configure criteria independently via tabbed editor.
- Schema: add nullable `category` to EvaluationForm with updated constraints
- Config: add `perCategoryCriteria` boolean to EvaluationConfigSchema
- Helper: new `findActiveForm()` with category-aware resolution + fallback
- Backend: getForm, upsertForm, getStageForm, startStage all category-aware
- AI services: use project category for form lookup in summaries + ranking
- Export/ranking: merge criteria from all active forms for cross-category reports
- Admin UI: toggle switch + tabbed criteria editor with per-category builders
- Jury UI: auto-selects correct form based on project category (invisible to juror)
- Fully backwards compatible: toggle defaults OFF, existing forms unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema:
- Drop 4 dead models: OverrideAction, NotificationPolicy, AssignmentException, AdvancementRule
- Drop 2 dead enums: OverrideReasonCode, AdvancementRuleType
- Drop 3 stale columns: Project.roundId, ConflictOfInterest.roundId, Evaluation.version
- Remove 3 back-relation fields from User, Assignment, Round
Code:
- Fix 6 COI queries in assignment.ts + 1 in juror-reassignment.ts
(roundId filter → assignment.roundId after column drop)
- Remove orphaned Project.roundId write in project.ts createProject
- Remove advancementRules include from round.ts getById
- Remove AdvancementRule from RoundWithRelations type
- Clean up seed.ts (remove advancement rule seeding)
- Clean up tests/helpers.ts (remove dead model cleanup)
- Add TODO comments on user delete mutations (FK violation risk)
Migration: 20260308000000_drop_dead_models_and_stale_columns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- z.any() replaced with z.record(z.string()) on webhook headers
- availabilityJson typed with z.array(z.object({ start, end }))
- Frontend webhook headers converted from array to Record before API call
- Docker HEALTHCHECK added to Dockerfile (health endpoint already existed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Prisma: connection_limit=10, pool_timeout=30 on DATABASE_URL in both compose files
- Graceful shutdown: SIGTERM/SIGINT forwarded to Node process in docker-entrypoint.sh
- testEmailConnection: replaced real email send with transporter.verify(), simplified UI to single button
- NotificationLog.userId index: confirmed already present, no change needed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Webhook HMAC: added consumer verification JSDoc with Node.js example using crypto.timingSafeEqual
- CSRF rate limiting: 20 requests/15min per IP on NextAuth /csrf endpoint
- Renamed withRateLimit to withPostRateLimit/withGetRateLimit for clarity
- 429 responses include Retry-After header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add escapeHtml() helper and apply to all user-supplied variables in 20+ HTML email templates
- Auto-escape in sectionTitle() and statCard() helpers for defense-in-depth
- Replace 5 instances of incomplete manual escaping with escapeHtml()
- Refactor bulkInviteTeamMembers: batch all DB writes in $transaction, then send emails via Promise.allSettled with concurrency pool of 10
- Fix inner catch block in award-eligibility-job.ts to capture its own error variable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>