15 KiB
Implementation Plan: Jury Voting
Overview
Build a full-stack web application for the 15. UNIKAT Ideenwettbewerb jury voting process. The app enables 6 jurors to score 11 teams on 5 criteria (1–10) via smartphones, with real-time results displayed on a beamer. Stack: React 19 + Vite frontend, Express.js backend, JSON file persistence, SSE for live updates. TypeScript throughout.
Tasks
-
1. Project scaffolding and configuration
-
1.1 Initialize Vite + React 19 + TypeScript project in
Jury-Voting/- Run
npm create vite@latestwith React + TypeScript template - Configure
vite.config.tswith proxy to Express backend (port 3001) - Add dependencies:
react-router-dom,lucide-react - Add dev dependencies:
vitest,fast-check,@testing-library/react,jsdom - Requirements: 5.2 (dark theme, Outfit font, glassmorphism)
- Run
-
1.2 Set up Express.js backend in
Jury-Voting/server/- Create
server/directory with TypeScript configuration - Install:
express,cors,tsx(for dev),typescript - Create
server/index.tsentry point with basic Express app on port 3001 - Add
npm run dev:serverscript usingtsx watch - Requirements: 3.1 (score persistence)
- Create
-
1.3 Define shared TypeScript interfaces in
Jury-Voting/shared/types.ts- Create
Session,Team,Juror,Criterion,Score,TeamResult,PendingScoreinterfaces - Create
SessionConfiginterface with criteria and scale settings - Export all types for use by both frontend and backend
- Requirements: 6.2, 6.3 (configurable teams, criteria, scale)
- Create
-
1.4 Create seed data file
Jury-Voting/server/data/seed.json- Pre-populate 11 teams from
Teams/folder (id, number, name, description) - Pre-populate 6 jurors (Cielejewski, Finke, Freyer, Knie, Meyer, Trieschmann)
- Pre-populate 5 criteria (Gesamtidee, Kundennutzen, Markt, Realisierbarkeit, Präsentation)
- Set default session config (scale 1–10)
- Requirements: 6.2, 6.3, 6.6
- Pre-populate 11 teams from
-
-
2. Backend data layer and core logic
-
2.1 Implement JSON file store (
server/store.ts)- Read/write
server/data/session.jsonatomically (write to temp, rename) - Create
.bakbackup before each write - Initialize from
seed.jsonifsession.jsondoesn't exist - Expose typed read/write functions for session, teams, jurors, scores
- Requirements: 3.1, 3.2 (persistence, reliability)
- Read/write
-
2.2 Implement score aggregation logic (
server/aggregation.ts)calculateTeamResult(teamId, scores[])→ total, average, jurorCount, completecalculateRankings(teamResults[])→ sorted results with dense ranking (ties get same rank)getCompletionStatus(jurorId, teamId, scores[])→ 'complete' | 'partial' | 'pending'- Pure functions, no side effects — easy to test
- Requirements: 4.2, 4.3, 4.4, 4.5
-
* 2.3 Write property test: Score Aggregation Correctness
- Property 4: Score Aggregation Correctness
- Generate random valid scores (1–10) for arbitrary juror/team/criterion combinations
- Assert: total equals arithmetic sum of all score values for a team
- Assert: average equals total / distinct jurors who scored
- Validates: Requirements 4.2, 4.3
-
* 2.4 Write property test: Ranking Correctness with Ties
- Property 5: Ranking Correctness with Ties
- Generate random team totals, compute rankings
- Assert: rankings sorted descending by total
- Assert: equal totals receive equal rank numbers
- Validates: Requirements 4.4, 4.5
-
* 2.5 Write property test: Team Completion Status
- Property 1: Team Completion Status
- Generate random subsets of criteria scores for a juror-team pair
- Assert: 5 scores → 'complete', 1–4 → 'partial', 0 → 'pending'
- Validates: Requirements 2.4, 7.1
-
2.6 Implement last-write-wins conflict resolution (
server/conflict.ts)- Compare timestamps on incoming score vs stored score
- Accept score with later
updatedAttimestamp - Return conflict info (409) if client sends older timestamp without force flag
- Requirements: 3.3, 3.4
-
* 2.7 Write property test: Last-Write-Wins Score Resolution
- Property 3: Last-Write-Wins Score Resolution
- Generate sequences of score updates with varying timestamps for same (jurorId, teamId, criterionId)
- Assert: final persisted value equals the value with the most recent timestamp
- Validates: Requirements 3.3
-
-
3. Backend API endpoints
-
3.1 Implement session endpoints
GET /api/session— return current session statePOST /api/session/start— set status to 'active', record startedAtPOST /api/session/end— set status to 'ended', record endedAt- Validate: cannot start if already active, cannot end if not active
- Requirements: 6.4, 6.5
-
3.2 Implement team and juror endpoints
GET /api/teams— return all teamsGET /api/jurors— return all jurors- Requirements: 6.2, 6.6
-
3.3 Implement score endpoints
POST /api/scores— submit/update a score (validate: session active, score 1–10, valid jurorId/teamId/criterionId)GET /api/scores/:jurorId— return all scores for a jurorGET /api/scores— return all scores (admin only)- On successful score submission, trigger SSE broadcast
- Requirements: 2.3, 3.1, 3.3
-
* 3.4 Write property test: Session Gate
- Property 6: Session Gate
- Generate score submissions with random session states ('setup', 'active', 'ended')
- Assert: accepted if and only if session status is 'active'
- Validates: Requirements 6.4, 6.5
-
3.5 Implement results and progress endpoints
GET /api/results— return aggregated TeamResult[] with rankingsGET /api/progress— return juror completion matrix (jurorId → count of fully-scored teams)GET /api/export/csv— generate and return CSV file- Progress endpoint must NOT include individual score values
- Requirements: 4.1, 7.1, 7.3, 8.1
-
* 3.6 Write property test: Progress Privacy
- Property 7: Progress Privacy
- Generate random score sets, call progress endpoint logic
- Assert: response contains juror completion counts but no individual score values
- Validates: Requirements 7.3
-
-
4. SSE real-time infrastructure
-
4.1 Implement SSE endpoint (
server/sse.ts)GET /api/results/stream— establish SSE connection- Maintain list of connected clients
- On score change, broadcast
scores_updatedevent with full results payload - Handle client disconnect (remove from list)
- Set appropriate headers:
Content-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive - Requirements: 4.1, 5.3
-
4.2 Create frontend SSE hook (
src/hooks/useSSE.ts)- Custom React hook that connects to
/api/results/stream - Parse incoming events and update local state
- Auto-reconnect on connection loss (EventSource built-in)
- Return:
{ results, connected, lastUpdate } - Requirements: 5.3 (update within 2 seconds without page refresh)
- Custom React hook that connects to
-
-
5. Checkpoint - Backend complete
- Ensure all tests pass, ask the user if questions arise.
-
6. Frontend: Juror selection and routing
-
6.1 Set up React Router with route structure
- Configure routes:
/,/vote,/vote/:teamId,/results,/admin - Create
App.tsxwithBrowserRouterand route definitions - Create layout wrapper with dark theme base styles
- Requirements: 1.1, 2.1
- Configure routes:
-
6.2 Implement
JurorSelectpage (src/pages/JurorSelect.tsx)- Fetch juror list from
GET /api/jurors - Display 6 juror names as large glass-panel cards (tap targets for mobile)
- On selection: store jurorId in localStorage, navigate to
/vote - Show warning if juror already has active session (allow override)
- Requirements: 1.1, 1.2, 1.3, 1.4
- Fetch juror list from
-
* 6.3 Write unit tests for JurorSelect
- Test: renders 6 juror names
- Test: selection stores jurorId and navigates
- Test: shows conflict warning for active juror
- Requirements: 1.1, 1.2, 1.3
-
-
7. Frontend: Voting interface
-
7.1 Implement
VotingDashboardpage (src/pages/VotingDashboard.tsx)- Fetch teams from
GET /api/teamsand scores fromGET /api/scores/:jurorId - Display 11
TeamCardcomponents in a scrollable list - Each card shows: team name, description snippet, status icon (✓ complete / ◐ partial / ○ pending)
- Tap card navigates to
/vote/:teamId - Requirements: 2.1, 2.4, 2.5
- Fetch teams from
-
7.2 Implement
TeamScoringpage (src/pages/TeamScoring.tsx)- Display team name and description at top
- Render 5
ScoreSlidercomponents (one per criterion) - Each slider: range 1–10, shows current value, large touch target
- Auto-save on change with 300ms debounce
- Show green flash confirmation on successful save
- Prev/Next team navigation buttons
- Requirements: 2.2, 2.3, 2.5
-
7.3 Implement offline score queue (
src/hooks/useOfflineQueue.ts)- On score change: save to localStorage
pendingScoresarray immediately - Attempt POST to
/api/scores— on success, mark as synced - On network failure: keep in queue, show subtle "offline" indicator
- On reconnect: replay pending scores in order
- Exponential backoff for retries (max 3 attempts per score)
- Requirements: 3.1, 3.2
- On score change: save to localStorage
-
* 7.4 Write property test: Offline Queue Preservation
- Property 2: Offline Queue Preservation
- Generate random sequences of scores submitted while "offline"
- Simulate sync, assert all scores present on server with original values/timestamps
- Validates: Requirements 3.2
-
* 7.5 Write unit tests for TeamScoring
- Test: renders 5 criteria with correct labels
- Test: auto-saves on slider change (debounced)
- Test: shows confirmation flash on save
- Test: prev/next navigation works
- Requirements: 2.2, 2.3
-
-
8. Frontend: Results view (beamer)
-
8.1 Implement
ResultsViewpage (src/pages/ResultsView.tsx)- Full-screen layout, no navigation chrome
- Connect to SSE via
useSSEhook - Display
RankingTablecomponent with animated transitions - Show "Voting in Progress" or "Final Results" based on session status
- Optimized for 1920×1080 landscape projection
- Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6
-
8.2 Implement
RankingTablecomponent (src/components/RankingTable.tsx)- Display ranked list: rank, team name, total score, average score
- Animate rank changes (CSS transitions on position)
- Highlight top 3 teams with special styling
- Handle ties (same rank number displayed)
- Requirements: 4.4, 4.5, 5.4
-
* 8.3 Write property test: Results Display Completeness
- Property 9: Results Display Completeness
- Generate random TeamResult objects, render ResultsView
- Assert: each team's name, total, average, and rank are present in rendered output
- Validates: Requirements 5.4
-
-
9. Frontend: Admin panel
-
9.1 Implement
AdminPanelpage (src/pages/AdminPanel.tsx)- Session controls: Start Voting / End Voting buttons with confirmation dialogs
- Show current session status and timestamps
- Warning when ending session with incomplete scores
- Requirements: 6.1, 6.4, 6.5
-
9.2 Implement
ProgressMatrixcomponent (src/components/ProgressMatrix.tsx)- Fetch from
GET /api/progress - Display 6×11 grid (jurors × teams) with completion indicators
- Show per-juror totals (X/11 teams complete)
- Indicate when all voting is complete (6/6 jurors × 11/11 teams)
- No individual scores shown — only completion status
- Requirements: 7.1, 7.2, 7.3
- Fetch from
-
9.3 Implement CSV export functionality
- Export button calls
GET /api/export/csvand triggers download - CSV format: teams as rows, (juror × criterion) as columns, plus totals/averages/ranks
- Match structure of original Auswertungstabelle
- Requirements: 8.1, 8.2, 8.3, 8.4
- Export button calls
-
* 9.4 Write property test: Export Round-Trip
- Property 8: Export Round-Trip
- Generate complete score sets, export to CSV, parse CSV back
- Assert: all individual scores recovered, totals correct, averages correct, rankings correct
- Validates: Requirements 8.1, 8.2, 8.3, 8.4
-
-
10. Checkpoint - Core features complete
- Ensure all tests pass, ask the user if questions arise.
-
11. Styling and visual polish
-
11.1 Implement global theme and CSS variables
- Define CSS custom properties:
--bg-primary: #0a0a0a,--accent: #00FF00, glassmorphism values - Import Outfit font from Google Fonts
- Set up glassmorphism utility classes (backdrop-filter, border, shadow)
- Dark scrollbar styling, smooth transitions
- Requirements: 5.2
- Define CSS custom properties:
-
11.2 Style all components to match Projekt-KIQ-HP design
- JurorSelect: glass-panel cards with hover/tap glow effect
- VotingDashboard: team cards with status indicators, neon green accents
- TeamScoring: custom slider styling (neon green track), score display
- ResultsView: large typography, animated rank transitions, glassmorphism table
- AdminPanel: clean grid layout, status badges
- Mobile-first responsive design (320px–428px primary, scales up)
- Requirements: 2.1, 5.1, 5.2
-
-
12. Deployment configuration
-
12.1 Create production build setup
- Configure Vite to build static assets to
dist/ - Configure Express to serve static files from
dist/in production - Single-port deployment: Express serves both API and frontend
- Add
npm run buildandnpm startscripts - Requirements: all (deployment readiness)
- Configure Vite to build static assets to
-
12.2 Create
docker-compose.ymlfor containerized deployment- Single container: Node.js serving Express + static frontend
- Volume mount for
server/data/(persist scores across restarts) - Expose port 3001 (or configurable via env)
- Add
.dockerignoreandDockerfile - Requirements: all (deployment readiness)
-
-
13. Final checkpoint - All features complete
- Ensure all tests pass, ask the user if questions arise.
Notes
- Tasks marked with
*are optional property-based tests — skip for faster MVP delivery given the tight timeline (event: 19. Mai 2026) - Each task references specific requirements for traceability
- Checkpoints at tasks 5, 10, and 13 ensure incremental validation
- Property tests validate the 9 correctness properties from the design document
- The app is designed for single-event use — simplicity over scalability
- All seed data (teams, jurors, criteria) is pre-populated; no manual setup needed at event time