Interview Unified Portal
Application review → interview day → consolidated ranking · honest scoring min(rubric 5.8, matrix 5.4) · status PARTIAL (Lighthouse not run; caps applied)
The August remediations held: one system, one auth, cycle management, hardened RLS. What stands between this platform and a smooth season is readiness, not architecture: applicant data lives in last year's records with zero materials attached, one cycle out of date, 28 date rules keyed to old calendar dates, and containers one reboot away from downtime.
Where the platform stands
Findings heatmap
| Phase | P0 | P1 | P2 | P3 |
|---|---|---|---|---|
| Code review | 1 | 3 | 5 | 2 |
| Data integrity | 2 | 0 | 1 | 0 |
| Config/Deps | 0 | 2 | 1 | 1 |
| Deploy/Ops/DR | 0 | 3 | 2 | 0 |
| UX/Flow/Reporting | 0 | 1 | 3 | 3 |
| Compliance/Obs/Tests | 0 | 0 | 0 | 2 |
Critical — 5 data/ops blockers + the flagship build
Last year's records carry zero materials — and every field was typed by hand
The gap is pipeline readiness, not an emergency import. Last year's manual-entry burden (123 applicants typed one by one) is exactly what this season's build removes.
Fix: build the auto-import pipeline (P0-6) so new PDFs flow straight into complete records. Last-year data is now backed up — 12 tables, verified.
Cycle still 2025-2026; stray Sept-8 test session pollutes assignments
Every cycle-scoped query defaults to last season; the test session can leak into session lists and kiosk.
Fix: archive 2025-2026 → create 2026-2027 at /admin/cycles (15 min); delete the stray session + assignments (5 min).
28 hardcoded date rules keyed to last year's interview dates
Einstein-only-Oct-30/31 and special Nov-24/Dec-11 rules live against literal calendar dates. New session dates silently mis-bucket applicants.
Fix: extract to per-session flags/config; 4 hr. InterviewerRoomView already points the way.
Containers not in docker-compose — one reboot kills interview day
Fix: add to compose, stop the deprecated container entirely (its app was absorbed by /evaluation). 30 min.
Score INSERT policy scoped to {public}; no audit trail on score edits
Fix: re-verify WITH CHECK enforces room assignment (interviewer_1/2/3_id match); add score-change history. last_edited_by exists; history doesn't.
Auto-import: PDF in → complete applicant record out. No AI, no external servers
The flow: admin drops the applicant PDF → parse → extract name, AAMC/external ID, medical school, USMLE, LOR summaries — and crop the applicant photo directly out of the PDF (embedded-JPEG or portrait-region render) → Storage upload → full applicant row created → review queue shows it instantly. Low-confidence fields route to a "confirm before publish" card, never silent wrongness.
Fix: extend upload-applicant-pdf into a parse-and-create pipeline; deterministic passes only; admin sees which fields were auto-extracted vs hand-confirmed. 1–2 d. Deadline: live before the PDFs land.
High-severity — 9 findings
Interview scores are a single 1–5 rating; review uses 7 categories × 1–10 — the unified ranking averages both
Fix: pick one scale for both stages before interview day — recommendation: 7 categories, 1–5, matching existing form copy — so ranking numbers become comparable.
Reviewers see all 123 applicants — no assignment scoping, no progress tracking
Fix: per-reviewer applicant assignment table + filter + progress bar. Personnel note (Sept 14): the evaluation committee and interview panels may be different people, and interviewer rosters can differ per session date — reviewer assignments and interviewer assignments are TWO separate models (reviewer→applicants; interviewer→rooms per session as the schema already supports). The single biggest reviewer-experience upgrade for review week.
Zero tests on scoring math, assignment generation, rotation timers, date filters
Fix: add vitest; start with the extracted date-helper (pairs with P0-3 fix) and score-aggregation tests.
1.58 MB single JS bundle; no route-level lazy loading
Fix: lazy() per domain (admin / interviewer / kiosk / evaluation) — same pattern that cut an Admin chunk on Unified Platform from 450 KB to ~80 KB.
No backup automation for season data; rollback is a manual dist.bak
Fix: nightly pg_dump of urology_* to a VPS volume. 2 hr.
Interviewer day-of flow doesn't surface paper-stage context by default
Fix: render 3-line paper-stage highlights on the room card — makes "one system" feel real to faculty in the room.
Cycle archive captures counts, not data exports
Fix: archive step writes a CSV/JSON export of the closing cycle.
No 48h interviewer prep email pipeline
Fix: cron-driven email from that session's own interviewer roster (rosters can differ between dates) + assigned applicants + 3-line paper highlights — prefaced "from the evaluation committee" since interviewers may not be reviewers. 4–6 hr.
Medium / low — 20 findings
| Sev | Finding | Where / effort |
|---|---|---|
| P2 | tsconfig strict=false + strictNullChecks=false; 93 any | enable strict on interview/* first · 3 hr |
| P2 | 98 console.log in src — payload leaks to user consoles | strip + structured logger · 2 hr |
| P2 | Destructive overwrite-assignments with no undo | confirm + snapshot · 2 hr |
| P2 | reviewer_name TEXT, no FK to reviewers/interviewers | migration + backfill · 3 hr |
| P2 | Legacy duplicate login pages shipped in bundle | delete 3 legacy pages · 1 hr |
| P2 | No error tracking; console-based "logging" | lightweight tracker · 2 hr |
| P2 | No WCAG run; contrast untested | axe/Lighthouse pass · 2 hr |
| P2 | temp-applicant-pdf.pdf ships in prod bundle | delete · 5 min |
| P2 | No per-reviewer completion report (needed daily in review week) | AdminReports extension · 4 hr |
| P3 | Dual lockfiles (bun.lockb + package-lock.json) | pick one · 15 min |
| P3 | README Lovable default | rewrite · 2 hr |
| P3 | InterviewSystem.tsx 3542-line god component | multi-session decomposition · 2–3 d |
| P3 | Applicant-facing itinerary portal | future season · P3 |
| P3 | No CI (lint/build/tests) | GitHub Actions basic · 3 hr |
| P3 | Rotation-start concurrent-trigger race (no optimistic locking) | admin-only + confirm · 1 hr |
Highest-value, lowest-effort
The four journeys, as built
Sees all 123 applicants, self-tracks progress
Needed this week: assigned-only view + progress bar (P1-2) — review week is 5–10 reviewers × ~12 applicants.
Best surface in the app — timed rotations, realtime updates
Paper-stage context exists in ApplicantProfileDialog but sits one click outside the default flow (P1-7) — surface it on the room card.
Tooling-rich, but cross-stage work feels like six separate tools
Admin ribbon (Review % → Interview Day → Ranking %) is the design fix.
Nothing — no portal, no itinerary
Applicant itinerary portal (arrival instructions, room rotation, day timeline) is on the 6-month roadmap.
Architecture 6.5
Shape
Single SPA → Supabase direct (no BFF). Auth via UnifiedAuthProvider + has_urology_role() RPC; three roles (admin/interviewer/evaluation). Cycle context (InterviewCycleContext) defaults to hardcoded '2025-2026'.
Component inventory
urology_reviews.reviewer_name TEXT (no FK) vs urology_interview_scores.interviewer_id UUID — schema split persistsBoundary problems
1. InterviewSystem.tsx = 3542L god component — evaluation UI, review API calls, categories, DNR logic all in one file.
2. No service/repository layer — 26 components call supabase.from('urology_*') inline (268 total call sites), no central query builders.
3. Hardcoded session-date logic (28 sites) in 5 files — business rule "Einstein-only on Oct 30/31; special flow Nov 24/Dec 11" is buried in components against literal calendar dates. Breaks the moment interview dates change (i.e. this season).
4. Legacy login pages (3 login flows + unified) still in bundle.
Lifecycle data flow (verified in code)
urology_reviews (paper stage, reviewer_name TEXT) → read in ApplicantProfileDialog + ConsolidatedRankingBoard + InterviewerRoomView (paper context before interview) → urology_interview_scores (single rating 1–5 per room, notes, question_comments jsonb) → ConsolidatedRankingBoard merges avg review + avg interview per applicant → urology_final_selections (rank_order, decision). Chain EXISTS but interview scale ≠ review scale (see 32).
Code Review — full findings + after-lenses 5.5
Method: source sync from VPS (312 files), grep sweeps, build/tsc probes in throwaway copy.
Findings
| ID | Sev | Finding | File:line evidence |
|---|---|---|---|
| CR-01 | P0 | Hardcoded session-date business rules: 28 sites across 5 files filter Einstein applicants and special scheduling against literal dates Oct 30/31, Nov 24, Dec 12 — last year's dates. New season dates will silently mismatch logic. | InterviewManagement.tsx:143-148, InterviewSessionDetail.tsx:276-491, InterviewerDashboard.tsx:157-183, InterviewSessions.tsx:72, (InterviewerRoomView.tsx:399 comment says "use configurable flag instead of hardcoded dates" — half-migrated) |
| CR-02 | P1 | Zero tests — 0 unit, 0 integration, 0 E2E. Scoring math + assignments logic untested. | find *.test.* = empty |
| CR-03 | P1 | InterviewSystem.tsx 3542L god component; 3 more >1000L. | wc -l: InterviewSystem 3542, InterviewerRoomView 1304, RankingApplicantCard 1065, ConsolidatedRankingBoard 1009 |
| CR-04 | P1 | 98 console.log left in src (leaks query payloads to browser console on any user's machine). | grep console.log = 98 |
| CR-05 | P2 | 93 : any types. | grep |
| CR-06 | P2 | tsconfig strict=false, strictNullChecks=false, noUnused*=false across app configs. Build is clean only because type-checking is off. | tsconfig.app.json/tsconfig.json |
| CR-07 | P2 | Destructive copy with no undo: "Any existing assignments in other sessions will be overwritten." | bundle copy |
| CR-08 | P2 | Scale copy mismatch: UI text says "1 = Poor…5 = Excellent" (single-rating form writes total_score=rating) while review table schema has 7 separate 1–10 columns. Numeric/graphic scale labels from dead code could confuse. | InterviewSystem.tsx:2817, InterviewScoreForm.tsx:117 total_score: rating |
| CR-09 | P2 | 142 catch blocks; several .catch(()=>{}) patterns risk silent failures on admin flows. | grep |
| CR-10 | P3 | Legacy Lovable README ("Welcome to your Lovable project") — zero operational docs. | README.md |
| CR-11 | P3 | No Ceslint pre-commit; eslint config exists but no CI to run it. | package.json scripts |
| CR-12 | P2 | Magic cycle fallback '2025-2026' baked in 4 sites of EditApplicantDetailsDialog + context default. | EditApplicantDetailsDialog.tsx:67-81, InterviewCycleContext.tsx:23 |
After-lens verdicts
Data Integrity (live DB) 4.5
| Finding | Sev | Evidence |
|---|---|---|
| urology_reviews = 0, urology_final_selections = 0, urology_interview_scores = 0 — no data from last season survives in tables; the "last-year data" is raw applicants only | P0 (data reality, not code) | live counts |
| All 123 applicants: pdf_url, photo_url, usmle_score, lors ALL null — these are LAST year's records; everything was typed manually last year. New applications arrive in ~1 week → the fix is the auto-import pipeline (P0-6), not a manual data dump | reframed Sept 14 | live SQL + user confirmation |
| Cycle stuck at 2025-2026 (is_current=t) + stray 2026-09-08 session created Sep 2 with 2 confirmed assignments — leftover test artifact | P1 | sessions table |
| seed-vs-live drift: FK constraints + cycle scoping added Aug, but data was never exercised end-to-end | P2 | 0 rows end tables |
| Category data: 115 regular, 5 i-sub, 2 sub-i, 1 einstein — matches Einstein-only date logic | info |
Dependencies 6.0
npm ci clean-install PASSED in throwaway VPS copy (0 missing) — declared-vs-installed OKnpm audit run (offline constraint of session) — flagged PARTIALTest Health 1.0
| Unit/integration/E2E | 0 |
| CI | none |
| Score anchor | testing = 1.0/10 (mechanical: zero test files) |
High-risk untested logic: assignment generation+overwrite, rotation timers/pause math, score aggregation (rounded avg formulas), Einstein date filters, RLS-protected admin flows.
Documentation 3.0
find = absent) — exists only in GitHub repo docs? Verify.API Surface & Data Layer 6.0
No REST backend: all data ops = Supabase client calls direct.
Edge functions (10)
import-applicants, import-new-applicants, clean-and-import-applicants, add-single-applicant, upload-applicant-pdf, upload-single-pdf, pdf-proxy, bootstrap-admin, create-interviewer-auth, reset-interviewer-password
Client query surface (verified counts)
| Table | call sites |
|---|---|
| urology_interview_scores | 45 |
| urology_interview_rotations | 39 |
| urology_applicants | 32 |
| urology_interview_assignments | 26 |
| urology_interview_rooms | 25 |
| urology_interview_sessions | 19 |
| urology_reviews | 18 |
| urology_interviewers | 15 |
| urology_final_selections | 13 |
| urology_interview_cycles | 6 |
| urology_user_roles | 2 |
| urology_reviewers | 1 |
Cross-ref issues
Mobile & Responsiveness 5.0
Design Quality & Strategy 6.5
Radix/shadcn primitives + a genuinely strong kiosk visual system (glow-success, kiosk-stat-card-compact, animate-scale-in, dark/light kiosk themes); tailwind token usage mostly present; large-type day UI reads well at distance.
Weak points: raw slate-* classes bypass tokens in interview components; AdminReports (939L) density risks clutter; the zero-PDF empty state is not designed (immediately relevant given materials gap).
Design strategy — this season
① ONE applicant card across all 3 stages (review → interview → ranking): same card, stage-scoped sections. Faculty feel one product. ② Kill raw slate-* → consolidate tokens. ③ Admin progress ribbon: Review % → Interview Day → Ranking %, persistent across admin pages. ④ Designed empty states for the zero-materials review queue ("Waiting on materials — 123 applicants pending") instead of blank cards. ⑤ Kiosk stays photo-free (anon SELECT surface — keep it that way when photos arrive).
Production, Deploy & Disaster Recovery 4.0
Score: 4.0
pg_dump of all 12 urology_* tables (schema+data) → /var/backups/interview-season/urology_full_20260914_pre_season.sql (VPS) + /workspace/backups/urology_full_20260914_pre_season.sql (local copy). Verified: 12 COPY sections, restorable with a single psql command.Score: 2.0 (P1)
TypeScript Audit 4.0
tsc --noEmit exit 0 (throwaway VPS copy) = clean against CURRENT (loose) configPerformance Audit 6.0
Score: 6.0 (no live Lighthouse — negotiate cap 8.0; mechanical evidence supports 6)
Auto-import note (Sept 14)
Extraction pipeline is LOCAL-ONLY: PyMuPDF/pdfminer + regex + tesseract OCR, all verified installed on the VPS — no AI provider, no external API, sensitive applicant data never leaves the server.
Page Flow Audit (journeys end-to-end) 5.5
Key journeys traced in code + bundle (no live-browser walkthrough this run — PARTIAL):
1. Reviewer review: login → /evaluation → pick applicant → 7-cat score + notes → save. Friction: no assignment-scoping → reviewer sees all 123.
2. Interviewer day: login → interviewer dashboard → schedule → room view → timed rotation → 1–5 rating + notes → submit. Timer + reset working. Paper-stage context only if admin opens profile dialog — not in the default interviewer flow (opportunity).
3. Admin ranking: /consolidated-ranking → drag-rank (dnd-kit) → final_selections write. avg review + avg interview MERGED but scale mismatch makes averages lie (CR-08).
4. No applicant-facing portal (planned gap).
Dead ends: legacy login routes (/admin-login etc) redirect to /login — good.
Integration Probes (live/build evidence) 5.0
| Probe | Result | Evidence |
|---|---|---|
| Live site | 200 | curl root |
| npm ci clean-install | PASS 0 missing | throwaway /tmp/ce-build on VPS |
| Ambient build (vite) | PASS 14.7s, 1.58MB chunk | build log |
| tsc --noEmit | PASS exit 0 | same |
| Unit tests | none (0) | repo find |
| Lighthouse | NOT RUN | no browser session available — caps applied |
| /api/health | n/a (SPA, no BFF) | route table |
| Compose coverage of running containers | FAIL — interview-portal-web + urology-review run manual docker run | Aug audit verified + current docker ps unchanged |
| urology_* backup (Sept 14) | PASS — 12 tables dumped (schema+data), VPS + local copy, COPY rows verified | |
| Edge functions deployed? | claimed live; import-applicants verify needed during PDF import |
Role Experience Audit 6.0
| Persona | Journey | Experience verdict |
|---|---|---|
| Reviewer (faculty) | /evaluation, all 123 applicants, must self-track progress | Raw list; no assignment scoping; no progress indicator. Needs P1-6 (own-applicants) urgently — institutional scale = 5-10 reviewers × 12 applicants each |
| Interviewer (faculty) | dashboard → schedule → room → score | Solid timed-rotation UX, realtime updates. Missing: paper-stage context in DEFAULT flow (it exists in profile dialog, one click away) + should see assigned applicants only |
| Admin (you) | 6 admin pages + interview mgmt + ranking | Tooling-rich; all 3 stages in one URL now; progress-ribbon absence makes cross-stage admin feel like 6 separate tools |
| Applicant | nothing | No portal, no itinerary (P3 roadmap item, matches season plan) |
August 2026 report — what happened to every finding since
The accepted August report recorded 21 P0/P1 findings, a 10-item ROI list, a 2-week sprint and a 4-quarter vision. Nothing is dropped: each item below is dispositioned against Sept-14 live evidence — resolved items kept as proven fixes, open items carried into today's severity lists.
Every finding the Aug 7-8 ce:full recorded is listed below with TODAY'S status (verified by the Sept 14 probes). Nothing from the accepted report is dropped: resolved items are kept as proven fixes, open items stay live findings.
P0 — Critical (6, August)
| Aug finding | Status Sept 14 | Evidence |
|---|---|---|
| P0-1 Two separate auth systems — no SSO (Review used hardcoded "Urology2026" visible in client JS; isAuthenticated defaulted true) | RESOLVED | review.srv absorbed into portal /evaluation; single Supabase Auth; deprecated container still running but unused (retire = P0-4 today) |
| P0-2 Neither container in docker-compose.yml | STILL OPEN | compose services ≠ running containers (verified Sept 14) |
| P0-3 No interview cycles in database (0 rows) | RESOLVED (residual) | 1 cycle exists (2025-2026, is_current=t) — but stuck on last season → P0-2 today |
| P0-4 Review system doesn't filter by interview_cycle | RESOLVED | cycle scoping active in 15/17 files; magic fallback remains (CR-12) |
| P0-5 Hardcoded password exposed in client bundle | RESOLVED | auth now Supabase + role RPC; old app deprecated |
| P0-6 No yearly setup workflow | RESOLVED (unused) | /admin/cycles wizard exists — not yet run for 2026-2027 |
P1 — High Impact (7, August)
| Aug finding | Status Sept 14 |
|---|---|
| Any authenticated user — incl. test accounts from other systems — can read/modify ALL urology data (47 auth users; permissive RLS) | PARTIALLY RESOLVED — Aug hardening rewrote 6 critical policies (44 live) but score INSERT still {public} (P0-5 today); 50 auth users now |
| Review API bypasses ALL RLS via service role key | RESOLVED BY DEPRECATION — app no longer the evaluation surface; key only matters if deprecated container stays up (retire it) |
| No cycle dropdown / archive indicator; importing 2026-2027 would blend cycles | RESOLVED (cycle context) but cycle not yet advanced |
| No loading states / Suspense boundaries | OPEN (P2-class) |
| Consolidated ranking route exists but data flow not wired | RESOLVED THEN REBROKEN: code reads both sources but averages incommensurable scales (P1-1 today) |
| No design consistency across 2 systems | RESOLVED — one system now |
| Reviewer email pipeline missing | STILL OPEN = P1-9 today; and departed reviewer (Dr. Reese) removed; reviewer_name TEXT FK issue open (P2) |
P2 / P3 (from August — still-relevant items retained)
| Aug finding | Status |
|---|---|
| Reviewer name TEXT not FK — no join possible | STILL OPEN (P2) |
| Sessions created Oct–Dec 2025 never populated; no cleanup for stale data | PARTIAL — archive exists; stray 9/8 test session still in DB (P0-2 today) |
| Cycle context used sessionStorage not localStorage | RESOLVED (Supabase-backed context now) |
| No CSV/bulk import for real applicant data | RESOLVED-BUT-UNUSED — edge functions exist; the Sept 14 plan is auto-import (P0-6) so no manual entry at all |
| No health endpoint, no backups before cycle transition | STILL OPEN (18/21) — except the one-time urology_* backup completed Sept 14 |
| 15+ Vercel-focused DEPLOYMENT files in Review (wrong platform) | MOOT — app deprecated |
| 20 unauthenticated rate-limited Review API routes | MOOT — deprecated |
| Zero tests in either system | STILL OPEN = P1-3 (0 test files) |
| Admin tables overflow on mobile (Review side) | MOOT; portal responsive grids good (09) |
August Roadmap — as written then, fulfilled (for the record)
- Week 1 (Stabilize & Secure): D1 compose · D2 create historical cycle row · D2 cycle filtering · D3 RLS harden · D4-5 remove departed reviewer + clean auth users + remove service-role key → all executed Aug 7-8 except compose (still open)
- Week 2 (Integrate & Launch): D6 yearly setup wizard · D7 CSV import tool · D8-9 merge Review into /evaluation · D9 wire kiosk/interviewer to real data · D10 cycle selector → executed Aug 7-8 (merge verified; wizard + cycle rows exist)
August 6-Month Vision (quarters as accepted then)
Q4 2025 (as planned): Stabilize & Integrate — DONE. Q1 2026: Unified portal + cycle mgmt — DONE. Q2 2026: Real-time interview day + consolidated ranking + analytics — built, data-empty (this season will exercise it). Q3 2026: Archive cycle · analytics dashboard · reviewer email pipeline — email pipeline still open (P1-9). Q3–Q4 2026: automation + year-2 wizard — superseded by Sept 14 season plan (P0-6 auto-import etc.).
August Feature Upgrades + ROI Ranking (status Sept 14)
| ROI item (as accepted: effort · payoff) | Status |
|---|---|
| Add cycle filtering — 2 hrs, unblocks yearly isolation | DONE Aug |
| Add containers to compose — 30 min, prevents silent loss on reboot | OPEN = P0-4 |
| Remove Dr. Reese — 5 min | DONE |
| Build setup wizard — 4-6 hrs, enables launch | DONE (unused — run it) |
| Build CSV import — 3 hrs, enables real applicant data | SUPERSEDED by P0-6 auto-import (no AI, local-only) |
| Harden RLS — 3 hrs | DONE Aug; gap remains (INSERT {public}) |
| Merge Review into Portal — 8-12 hrs, eliminates dual maintenance | DONE Aug |
| Wire kiosk + interviewer — 4 hrs | DONE Aug |
| Consolidated ranking — 4 hrs | DONE-but-scale-broken (P1-1) |
| Reviewer email pipeline — 3 hrs | OPEN = P1-9 |
August Feature Upgrades (the forward-looking list, then → now)
| August idea | Sept 14 disposition |
|---|---|
| Merge app + interview scores in unified ranking with drag-and-drop reorder | Exists; needs scale fix (P1-1) |
| Supabase Realtime for kiosk countdown, room monitor, live scoring | WORKING (verified in code) |
| Post-cycle analytics: score distributions, reviewer agreement (Cohen's kappa), cross-cycle comparison | On 6-month vision (Q1 2027) |
| Reviewer email pipeline for launch/assignments | OPEN (P1-9) |
| AI-assisted applicant screening (parse PDFs, flag criteria, suggest distribution) | SUPERSEDED & CONSTRAINED: P0-6 auto-import is deterministic + 100% local — no AI, no external servers (user privacy rule Sept 14) |
| Mobile-first interview-day scoring | Exists (rotation-timed scoring UI); perf + a11y gaps documented (09/16) |
Business Logic & Policy Audit 5.0
Edge-case matrix: whom does the date filter include/exclude?
| Session date | Logic says | Category filter |
|---|---|---|
| Oct 30/31 | Einstein only | category === 'einstein' |
| Nov 24, Dec 11, Dec 12 | special flow | category !== 'einstein' |
| Any other date | NO RULE — default path = regular applicants included, einstein excluded? (Option A in code depends on branch) — inactive rule = silent mis-bucketing if 2025-2026 dates shift |
Rating/averaging math (verified)
rating 1–5 → total_score (form says 1=Poor…5=Excellent). Round-to-1-decimal averages elsewhere (Math.round(x*10)/10).Policy: what happens when…
Lifecycle integration (the "one system" goal, in code)
Operational Reporting Audit 6.0
| Report | Answers real question? | Trustworthy? | Export? |
|---|---|---|---|
| AdminReports (CSV) | Yes (review progress) | Yes (once data exists) | CSV ✓ |
| Applicant Performance Rankings | avg score across sessions | Mixed — combines incommensurable scales | ? |
| RoomMonitor | real-time ops | Yes (realtime) | n/a |
| Interviewer progress (avg + count) | Yes | Yes | ? |
Missing: per-reviewer reviewer-assignment completeness report (who hasn't submitted what) — the one admin report needed daily during review week. Per-session day-of summary with quotes (competing feedback for the PD) — flagged as design-opportunity, not bug.
Backlog — epics, stories, story points 42 SP
| Epic | Stories | SP |
|---|---|---|
| Season Readiness | 5 | 13 |
| Unify Review/Interview Scale | 3 | 8 |
| Reliability & Tests | 4 | 13 |
| Faculty Experience | 3 | 8 |
| Total | 15 | 42 SP |
User stories
Season Readiness (13 SP): admin — current cycle 2026-2027 (0.5) · reviewer — PDFs visible inline (5) · dev — date rules config-driven (2) · admin — stray test data purged (0.5) · ops — containers in compose (5).
Unify Scale (8 SP): one scale across 7-cat review and day-of interview so ranking compares like with like.
Reliability & Tests (13 SP): vitest on date-helper, score aggregation, assignment overwrite; CI basics.
Why the live interview dashboard failed last year — and the fix, already applied
Realtime publication excluded every interview table — subscriptions ran but never received events
All 13 live components (room monitor, kiosk, interviewer room view, leaderboard, scoring progress) subscribe via postgres_changes. The subscriptions opened fine but got no events — the "live" dashboard looked dead because, downstream of the socket layer, it was.
Fix (applied to the live DB Sept 14): added the 5 interview tables to the publication (rotations, scores, assignments, rooms, sessions) — verified in pg_publication_tables; realtime container restarted healthy. This is a configuration fix, minutes not days.
Validation: before interview day, run one end-to-end socket test (insert a test rotation, observe a subscribed client react). That closes the loop with proof instead of hope.
If realtime misbehaves on the day: 5-second polling is already built in
RoomMonitor already has a 5s polling backup alongside its subscription (RoomMonitor.tsx:243). The built plan: extend that poll-fallback to the other day-of surfaces behind a toggle — live = realtime when healthy, degraded = 5s polling when not. Either way the room monitor and kiosk reflect the room.
Recommendation: don't skip the live dashboard. The root cause was configuration and is now fixed; the safest posture is realtime primary + polling fallback, validated with a live socket test before the first session.
Personnel are disjoint — review ≠ interview
Evaluation committee and interview panels are separate people — and interviewer rosters can differ between session dates
Two separate assignment models: reviewer→applicants (paper stage) and interviewer→rooms (per session, via the existing room interviewer slots). No global season roster is assumed anywhere.
Interviewer context: for an interviewer who wasn't on the review committee, the paper-stage highlights on their room card are prefaced "from the evaluation committee" — it's their first exposure to the applicant's paper record, not a reminder.
Per-session rosters: the 48h prep email and session views pull that session's own interviewer list. A person can interview on one date and not the other — the schema (per-room interviewer slots) already supports this; the tooling just can't assume continuity.
Reporting stays role-split: reviewer completeness (who hasn't finished paper reviews) and interviewer coverage (who's assigned to which rooms/rotations) are reported separately — a session-only interviewer having zero review rows is correct, not a red flag.
2-Week Sprint
- D1–2: P0-2 cycle purge (30 min) → P0-6 auto-import build begins — deadline: before PDFs land · 4 SP
- D3: P0-3 date-rule extraction + unit tests (4 hr) · 3 SP
- D4–5: P1-1 scale unification + P1-2 reviewer scoping · 5 SP
- D6–7: P1-7 paper-context on room cards + P1-9 48h prep email · 4 SP
- D8–10: P2 sweep: console.log, temp PDF, legacy logins, strict mode on interview/* · 4 SP
6-Month Vision
| Quarter | Theme |
|---|---|
| Q4 2026 | Scale parity · assignment scoping · per-reviewer reporting |
| Q1 2027 | Applicant itinerary portal · pulse surveys · end-of-day top-10 · post-cycle analytics |
Make interview day feel 100% better
- One applicant card across all 3 stages — review → interview → ranking share the same card, stage-scoped sections. Faculty feel one system, not three screens.
- Paper-stage highlights on room cards — "what the committee thought on paper" without leaving the rotation flow (the code already reads reviews; render it).
- 48h prep email — room + applicants + 3-line highlights per interviewer, auto-generated. You stop hand-prepping; faculty walk in prepared.
- End-of-day top-10 in room monitor within minutes of the final score — faculty see the product of their input same-day.
- Reviewer progress bar + own-applicants-only — review becomes a manageable assignment, not a 123-row gauntlet.
- Post-session pulse survey — 2 questions within an hour; the feedback loop that tunes the next session while the season is young.
Feature ideas
Auto-import (P0-6)
PDF in → full applicant record + photo out. Zero manual typing this year; 100% local extraction.
Per-reviewer completeness report
Daily review-week view: who's behind, on what.
Post-session pulse survey
2 questions ≤1 hr after each session; ops tuning while the season is young.
End-of-day top-10
Auto-computed minutes after the final score lands.
Applicant itinerary portal
Arrival instructions, room rotation, day timeline (6-month roadmap).
Admin progress ribbon
Review % → Interview Day → Ranking % across every admin page.
UI/UX & design polish
- Designed empty states for the zero-materials review queue
- Token cleanup: raw slate-* classes → design tokens
- Bundle lazy-loading — faster kiosk screens and interviewer phones
- Contrast/a11y pass before the season (footer text, small labels)
Design, Motion & Rendered-UX audit — 16 routes walked in a real browser
Authenticated headless-Chromium walkthrough (login → every route, screenshots + computed-style harvest) + per-page vision review. This is the missing rendered-lens pass: fonts, animations, density, empty states, and the kiosk — with per-page verdicts and scores.
Login was completely broken in production — auth pointed at a dead Supabase Cloud host
Three hardcoded cloud URLs lived in src (AdminSetupInterviewers.tsx ×2, PDFViewer.tsx). Fixed Sept 14: repointed to the self-hosted stack, rebuilt clean, container restarted, verified login → /admin/dashboard. The portal could not be logged into at all before this audit.
Method: authenticated headless Chromium walkthrough of all 16 routes (login → route loop, screenshots + computed-style harvest), page-by-page vision review, animation counts from getComputedStyle. Complements source-level 10a/10b — this is RENDERED evidence.
P0 discovered during the walkthrough (fixed same day)
Login was completely broken in production. The deployed bundle pointed auth at fsxzgkyiutpjobpmfgtk.supabase.co (an unresolvable Supabase Cloud host — DNS NXDOMAIN), while the self-hosted stack is supabase.srv1738752.hstgr.cloud. Symptom: "Failed to fetch" on every Sign In. Three hardcoded cloud URLs lived in src (AdminSetupInterviewers.tsx ×2, PDFViewer.tsx). Fixed: sed to self-hosted host, rebuilt (clean), restarted container, verified login → /admin/dashboard. This is the single highest-impact finding of the whole audit: the portal could not be logged into at all before today.
Font system (rendered)
| Surface | Display | Body | Notes |
|---|---|---|---|
| Admin/portal | DM Sans | DM Sans | consistent; SF Mono for codes/IDs |
| Kiosk display | Oswald (condensed caps) | DM Sans | good display choice for distance |
| Code/IDs | SF Mono | — | monospace used sparingly and correctly |
Verdict: font choices are coherent and modern; NOT a font problem. The problem is rendered SIZE and weight on kiosk labels (10–12px — unreadable at distance).
Motion / animation inventory (computed, not claimed)
| Surface | Animated elements | Verdict |
|---|---|---|
| All admin pages | 0 | static; fine for admin, but zero feedback animation (no toast/skeleton/transition presence) |
| Kiosk | 2 | only the clock ticks; Live pill, counters, progress bars are static → reads "frozen/broken" from a wall |
| Interviewer views | 0 detected (source has animate-scale-in on some cards but not observed rendering) | motion mostly theoretical |
The app is effectively animation-free in rendered output. Kiosk NEEDS motion (pulse, counter roll, bar fill) because "live" must be visible.
Per-page verdicts (rendered)
| Route | Verdict | Score | Top issues |
|---|---|---|---|
| /login | Clean / Acceptable | 7 | (was broken — P0 fixed); form fine, no password reset link, no error toasts beyond inline "Failed to fetch" |
| /admin/dashboard | Needs Work | 5.5 | Quick-actions 5+1 orphan row; truncated season selector ("2025–…" — critical context lost); "Go →" microcopy ×6; two 0-value KPIs not muted/explained; no primary CTA |
| /evaluation | Needs Work | 5.5 | KPI math mismatch (115+2+1=118 vs 123 total — trust breaker); "0/12" AND "0% complete" redundant; gray-on-gray progress bars invisible; no search/filter/drill-down; generic template feel |
| /admin/applicants | Acceptable w/ gaps | 6 | No search, no sort, no pagination (123 rows ≈ 12 viewport-scrolls), no bulk select/actions, per-row dropdowns for Decision + Date = 246+ individual interactions to schedule a season; delete has no confirm; Photo/PDF icons are unlabeled/ambiguous |
| /admin/reports | Acceptable | 6.5 | 2 tables render; limited filters; no export visible in walkthrough |
| /admin/cycles | Clean | 7 | clear create/archive affordances |
| /admin/interviewers | Thin | 5 | near-empty (0 rows); no empty-state guidance to add interviewers |
| /admin/rooms | Thin | 5 | same — empty grid with no "set up rooms" CTA |
| /admin/score-management | Acceptable | 6 | functional; dense labels small |
| /admin/setup-interviewers | Clean | 7 | wizard steps clear |
| /interviews | Acceptable | 6.5 | session list functional |
| /interview-sessions | Acceptable | 6.5 | fine |
| /room-monitor | Needs Work | 5.5 | empty state thin; live-data path was broken until today's realtime publication fix |
| /room-comments | Broken-feeling | 4 | renders 25 chars of text — effectively blank page |
| /consolidated-ranking | Needs Work | 5 | empty-state conflates two different errors ("no scores OR not authenticated") + "refresh if you just logged in" = known-flaky workaround; no column/score explanation; NOTE: also reveals scale-mismatch caveat buried in small print |
| /kiosk | Needs Redesign | 4.5 | 70% dead canvas; labels 10–12px unreadable at distance; zero-everything w/ no empty state (looks crashed); date contradiction (9/8 vs Sep 14); DUPLICATE session tabs (12:45/08:15 twice); color-only state encoding; dead motion; non-interactive chrome (tabs: brightness, X) on a wall display |
Cross-cutting design themes
1. Admin surfaces are clean-but-generic (Bootstrap-era card+pill vocabulary). Acceptable for internal use; not a differentiator. The quickest premium lift: density toggle, sticky headers, search/filters, semantic color on KPIs.
2. Kiosk is the one screen with a real design concept (dark display) — and it's the least finished. Fixing kiosk (scale ×2.5, live motion, empty states, dedup tabs) is the highest-value design deliverable for interview day.
3. Empty states are the systemic gap. Rooms, interviewers, comments, ranking, kiosk all fail the "is the system alive or broken?" test. A shared EmptyState component (icon + reason + one CTA) fixes six screens.
4. Data-trust UI issues undermine otherwise-good screens: the 118-vs-123 KPI mismatch and the ranking empty-state copy are the two places a faculty user loses confidence in 5 seconds.
5. Motion: the design system has animation classes in source, but rendered output is static. Either wire them (kiosk first) or stop shipping them.
Design sprint recommendations (interview-season priorities)
ACTION REQUIRED — 5.4 / 10
verified by ce:full v3.0.0 · Sept 14, 2026 · honest scoring min(rubric 5.8, matrix 5.4) · status PARTIAL (Lighthouse + network-auth probes not run; caps applied)
Full artifact set lives in /workspace/.hermes/ce/full/2026-09-14-interview-portal/ — 00 executive summary · 01 project profile · 02 architecture · 03 code review + after-lenses · 04 dependencies · 05 test health · 06 documentation · 07 TypeScript audit · 08 API surface · 08a data integrity · 09 mobile · 10 bundle · 10a/10b design · 11 roadmap · 12 backlog · 13–22 advanced phases · 23 remediation log · 24 perfection loop · 25 quality-gate matrix · 26–28 UX phases · 29 page flow · 30 integration probes · 31 role experience · 32 logic & policy · 33 operational reporting.