Initial monorepo structure

This commit is contained in:
2026-06-30 20:37:40 +02:00
commit 2f2b295531
121 changed files with 39171 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"specId": "e33fa6e5-89e0-4d8d-ad9e-d43d6bf23daa", "workflowType": "requirements-first", "specType": "feature"}
+475
View File
@@ -0,0 +1,475 @@
# Design Document: Jury Voting
## Overview
The Jury Voting app is a standalone web application that digitizes the paper-based evaluation process for the **15. UNIKAT Ideenwettbewerb**. It enables 6 jurors to score 11 teams on 5 criteria (110 each) via their smartphones, while a beamer/projector view shows live-updating rankings to the audience.
The system is designed for a single-event use case with minimal infrastructure requirements — deployable on a laptop connected to the projector, or a simple server. The architecture prioritizes simplicity, reliability, and a premium visual experience matching the Projekt-KIQ-HP design language.
**Key design decisions:**
- **Monorepo structure**: Single `Jury-Voting/` directory containing both frontend and backend
- **Express.js + JSON file backend**: Lightweight server with file-based persistence (no database needed for 6 jurors × 11 teams)
- **Server-Sent Events (SSE)** for real-time updates to the beamer view — simpler than WebSockets, no library needed, unidirectional (server → client) which is exactly what the results view needs
- **Optimistic UI with local fallback**: Scores are saved locally first, then synced to server. Handles brief connectivity loss gracefully.
- **No authentication complexity**: Simple name-based juror selection (no passwords) — appropriate for a trusted, in-person event
## Architecture
```mermaid
graph TB
subgraph "Client Devices"
J1[Juror Phone 1]
J2[Juror Phone 2]
J6[Juror Phone 6]
B[Beamer Browser]
A[Admin Laptop]
end
subgraph "Server (single process)"
API[Express.js REST API]
SSE[SSE Endpoint]
FS[JSON File Store]
end
J1 -->|POST /api/scores| API
J2 -->|POST /api/scores| API
J6 -->|POST /api/scores| API
A -->|Admin API calls| API
API -->|Read/Write| FS
API -->|Notify| SSE
SSE -->|EventStream| B
SSE -->|EventStream| A
```
### Architecture Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Backend | Express.js | Minimal setup, same language as frontend, easy to bundle |
| Persistence | JSON file | 6×11×5 = 330 score cells — trivial data volume, no DB overhead |
| Real-time | SSE (Server-Sent Events) | Unidirectional push is sufficient; no WebSocket library needed |
| Frontend | React 19 + Vite | Matches Projekt-KIQ-HP stack, fast HMR for development |
| Styling | CSS variables + glassmorphism | Reuses KIQ-HP design tokens, no CSS framework needed |
| Deployment | docker-compose OR `npm run dev` | Flexible: production-like or quick laptop setup |
| Offline handling | localStorage queue | Scores saved locally, synced on reconnect |
### Deployment Topology
```mermaid
graph LR
subgraph "Event Setup (laptop or server)"
V[Vite Dev Server :5173]
E[Express API :3001]
end
subgraph "Network (local WiFi)"
P1[Phone 1] --> V
P6[Phone 6] --> V
BM[Beamer PC] --> V
end
V --> E
```
For production, Vite builds static assets served by Express on a single port. For development, Vite proxies API calls to Express.
## Components and Interfaces
### Route Structure
| Route | Component | Purpose |
|-------|-----------|---------|
| `/` | `JurorSelect` | Juror name selection (landing page) |
| `/vote` | `VotingDashboard` | Team list with scoring status |
| `/vote/:teamId` | `TeamScoring` | Score input for a specific team |
| `/results` | `ResultsView` | Beamer-optimized live rankings |
| `/admin` | `AdminPanel` | Session management, progress, export |
### Frontend Component Tree
```mermaid
graph TD
App --> Router
Router --> JurorSelect
Router --> VotingDashboard
Router --> TeamScoring
Router --> ResultsView
Router --> AdminPanel
VotingDashboard --> TeamCard[TeamCard × 11]
TeamScoring --> ScoreSlider[ScoreSlider × 5]
TeamScoring --> TeamHeader
ResultsView --> RankingTable
ResultsView --> SessionStatus
AdminPanel --> ProgressMatrix
AdminPanel --> SessionControls
AdminPanel --> ExportButton
```
### Key Components
#### `JurorSelect`
- Displays 6 juror names as large tap targets (glass-panel cards)
- On selection, stores juror identity in localStorage and navigates to `/vote`
- Shows warning if juror is already active in another session
#### `VotingDashboard`
- Lists all 11 teams as cards
- Each card shows: team name, description snippet, scoring status (✓ complete / partial / not started)
- Tap navigates to `/vote/:teamId`
#### `TeamScoring`
- Shows team name + description at top
- 5 score inputs (one per criterion), each a slider or number stepper (110)
- Auto-saves on change (debounced 300ms)
- Visual confirmation on save (green flash)
- Navigation: prev/next team buttons
#### `ResultsView` (Beamer)
- Full-screen, no navigation chrome
- Animated ranking table with team names, scores, ranks
- Updates via SSE — smooth transitions when scores change
- Shows "Voting in Progress" or "Final Results" status
- Designed for 1920×1080 landscape
#### `AdminPanel`
- Session controls: Start/End voting session
- Progress matrix: 6 jurors × 11 teams grid showing completion
- Export button (CSV download)
- Juror/team configuration (pre-populated)
### API Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| `GET` | `/api/session` | Get current session state |
| `POST` | `/api/session/start` | Start voting session |
| `POST` | `/api/session/end` | End voting session |
| `GET` | `/api/teams` | List all teams |
| `GET` | `/api/jurors` | List all jurors |
| `GET` | `/api/scores` | Get all scores (admin) |
| `GET` | `/api/scores/:jurorId` | Get scores for a juror |
| `POST` | `/api/scores` | Submit/update a score |
| `GET` | `/api/results` | Get aggregated results |
| `GET` | `/api/results/stream` | SSE endpoint for live updates |
| `GET` | `/api/export/csv` | Download CSV export |
| `GET` | `/api/progress` | Get juror completion matrix |
### Score Submission Payload
```json
{
"jurorId": "finke",
"teamId": "dual",
"criterionId": "gesamtidee",
"score": 8
}
```
### SSE Event Format
```json
{
"event": "scores_updated",
"data": {
"results": [
{ "teamId": "dual", "teamName": "DUAL", "total": 187, "average": 31.2, "rank": 1 }
],
"timestamp": "2026-05-19T18:30:00Z"
}
}
```
## Data Models
### Session
```typescript
interface Session {
id: string;
status: 'setup' | 'active' | 'ended';
startedAt: string | null; // ISO timestamp
endedAt: string | null; // ISO timestamp
config: SessionConfig;
}
interface SessionConfig {
criteria: Criterion[];
scaleMin: number; // default: 1
scaleMax: number; // default: 10
}
```
### Team
```typescript
interface Team {
id: string; // kebab-case slug, e.g. "dual"
number: number; // original number from submission
name: string; // display name, e.g. "DUAL"
description: string; // short description
contact?: string; // contact person (optional)
}
```
### Juror
```typescript
interface Juror {
id: string; // lowercase name, e.g. "finke"
name: string; // display name, e.g. "Finke"
active: boolean; // currently has an active session
}
```
### Criterion
```typescript
interface Criterion {
id: string; // kebab-case, e.g. "gesamtidee"
nameDE: string; // German name, e.g. "Gesamtidee"
nameEN: string; // English name, e.g. "Overall Idea"
}
```
### Score
```typescript
interface Score {
jurorId: string;
teamId: string;
criterionId: string;
value: number; // 110
updatedAt: string; // ISO timestamp
}
```
### Aggregated Result
```typescript
interface TeamResult {
teamId: string;
teamName: string;
total: number; // sum of all juror scores across all criteria
average: number; // total / number of jurors who scored
rank: number; // 1-based, ties get same rank
jurorCount: number; // how many jurors have scored this team
complete: boolean; // all 6 jurors scored all 5 criteria
}
```
### JSON File Structure (persistence)
```json
{
"session": { "id": "unikat-2026", "status": "active", "startedAt": "...", "endedAt": null, "config": { ... } },
"teams": [ ... ],
"jurors": [ ... ],
"scores": [
{ "jurorId": "finke", "teamId": "dual", "criterionId": "gesamtidee", "value": 8, "updatedAt": "..." }
]
}
```
The entire state fits in a single JSON file (~50KB max). The server reads/writes this file atomically on each score submission.
### Score Aggregation Logic
```
Per team:
total = SUM(score.value) for all scores where score.teamId == team.id
jurorCount = COUNT(DISTINCT score.jurorId) where score.teamId == team.id AND all 5 criteria submitted
average = total / jurorCount (or total / 6 if all jurors scored)
Ranking:
Sort teams by total DESC
Assign rank 1, 2, 3... with ties getting the same rank (dense ranking)
Example: scores [250, 240, 240, 230] → ranks [1, 2, 2, 4]
```
### Offline Queue (localStorage)
```typescript
interface PendingScore {
jurorId: string;
teamId: string;
criterionId: string;
value: number;
timestamp: string;
synced: boolean;
}
```
When offline, scores are pushed to a `pendingScores` array in localStorage. On reconnect, the app replays them to the server in order. The server uses `updatedAt` to resolve conflicts (last-write-wins).
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Team Completion Status
*For any* juror and any set of submitted scores, a team's completion status SHALL be "complete" if and only if exactly 5 scores exist (one per criterion) for that juror-team pair, "partial" if between 1 and 4 scores exist, and "pending" if zero scores exist.
**Validates: Requirements 2.4, 7.1**
### Property 2: Offline Queue Preservation
*For any* sequence of scores submitted while offline, after synchronization completes, the server state SHALL contain all scores from the offline queue with their original values and timestamps preserved.
**Validates: Requirements 3.2**
### Property 3: Last-Write-Wins Score Resolution
*For any* sequence of score updates to the same (jurorId, teamId, criterionId) triple, the final persisted value SHALL equal the value of the most recent update (by timestamp).
**Validates: Requirements 3.3**
### Property 4: Score Aggregation Correctness
*For any* team and any set of valid scores (each 110), the total score SHALL equal the arithmetic sum of all individual score values, and the average SHALL equal the total divided by the number of distinct jurors who have submitted at least one score for that team.
**Validates: Requirements 4.2, 4.3**
### Property 5: Ranking Correctness with Ties
*For any* set of team totals, the ranking SHALL be sorted in descending order by total score, and any two teams with equal total scores SHALL receive the same rank number.
**Validates: Requirements 4.4, 4.5**
### Property 6: Session Gate
*For any* score submission attempt, the system SHALL accept the score if and only if the current session status is "active". Submissions when session status is "setup" or "ended" SHALL be rejected.
**Validates: Requirements 6.4, 6.5**
### Property 7: Progress Privacy
*For any* call to the progress endpoint, the response SHALL contain juror completion counts (number of fully-scored teams per juror) but SHALL NOT contain any individual score values.
**Validates: Requirements 7.3**
### Property 8: Export Round-Trip
*For any* complete set of scores, exporting to CSV and parsing the result SHALL recover: all individual scores per (juror, team, criterion), correct aggregated totals, correct averages, correct rankings, with teams as rows and (juror × criterion) as columns.
**Validates: Requirements 8.1, 8.2, 8.3, 8.4**
### Property 9: Results Display Completeness
*For any* TeamResult object, the rendered results view SHALL include the team name, total score, average score, and current rank.
**Validates: Requirements 5.4**
## Error Handling
### Network Errors (Juror Phones)
| Scenario | Handling |
|----------|----------|
| Score submission fails (timeout/5xx) | Store in localStorage queue, show subtle "offline" indicator, retry on reconnect |
| Server unreachable | Switch to offline mode, continue scoring locally, sync when back |
| Partial sync failure | Retry individual failed scores with exponential backoff (max 3 retries) |
### Data Validation Errors
| Scenario | Handling |
|----------|----------|
| Score out of range (< 1 or > 10) | Reject at client and server, show validation error |
| Unknown jurorId or teamId | Return 400 with descriptive error message |
| Score submission when session not active | Return 403 with "Session not active" message |
| Duplicate score without overwrite | Return 409 with current value, client prompts for confirmation |
### Server Errors
| Scenario | Handling |
|----------|----------|
| JSON file write failure | Return 500, log error, scores remain in client queue |
| JSON file corruption | Keep backup copy (`.bak`), restore from backup on startup |
| SSE connection dropped | Client auto-reconnects with `EventSource` built-in retry |
### Admin Errors
| Scenario | Handling |
|----------|----------|
| End session with incomplete scores | Show warning with progress summary, require confirmation |
| Export with no scores | Return empty CSV with headers only |
## Testing Strategy
### Property-Based Testing
**Library:** [fast-check](https://github.com/dubzzz/fast-check) (JavaScript/TypeScript PBT library)
Property-based tests will validate the 9 correctness properties defined above. Each test runs a minimum of **100 iterations** with randomly generated inputs.
**Test configuration:**
```typescript
fc.assert(fc.property(...), { numRuns: 100 })
```
**Tag format:** Each test file includes a comment referencing the design property:
```typescript
// Feature: jury-voting, Property 4: Score Aggregation Correctness
```
**Properties to test:**
1. Team completion status computation (pure function)
2. Offline queue sync logic (mock server)
3. Last-write-wins resolution (pure function)
4. Score aggregation (pure function — sum and average)
5. Ranking with ties (pure function — sort + rank assignment)
6. Session gate logic (pure function — accept/reject based on status)
7. Progress privacy (API response shape validation)
8. Export round-trip (generate scores → export CSV → parse → compare)
9. Results display completeness (render → check fields present)
### Unit Tests (Example-Based)
Unit tests cover specific scenarios, edge cases, and integration points:
- **JurorSelect**: Renders 6 names, handles selection, shows conflict warning
- **TeamScoring**: Renders 5 criteria, auto-saves on change, shows confirmation
- **VotingDashboard**: Shows correct status icons for complete/partial/pending teams
- **ResultsView**: Shows "Voting in Progress" vs "Final Results" based on session state
- **AdminPanel**: Session start/end controls, export button
- **API endpoints**: Correct HTTP status codes, validation errors, auth checks
- **SSE**: Connection, reconnection, event parsing
### Integration Tests
- Full scoring flow: juror selects name → scores a team → results update
- Offline/online cycle: score while offline → reconnect → verify sync
- Session lifecycle: create → start → score → end → export
- Concurrent jurors: 6 simultaneous score submissions
### Test Structure
```
Jury-Voting/
├── src/
│ ├── __tests__/ # Unit tests (co-located)
│ └── ...
├── server/
│ ├── __tests__/ # Server unit tests
│ └── ...
└── tests/
├── properties/ # Property-based tests
│ ├── aggregation.property.test.ts
│ ├── ranking.property.test.ts
│ ├── completion.property.test.ts
│ ├── session-gate.property.test.ts
│ ├── offline-queue.property.test.ts
│ ├── last-write-wins.property.test.ts
│ ├── progress-privacy.property.test.ts
│ ├── export-roundtrip.property.test.ts
│ └── results-display.property.test.ts
└── integration/ # Integration tests
├── scoring-flow.test.ts
└── session-lifecycle.test.ts
```
### Test Runner
- **Vitest** — fast, Vite-native, supports TypeScript, works with fast-check
- Run: `npx vitest --run` (single execution, no watch mode)
+192
View File
@@ -0,0 +1,192 @@
# Requirements Document
## Introduction
This feature replaces the paper-based jury evaluation process (Bewertungsbogen and Auswertungstabelle) for the **15. UNIKAT Ideenwettbewerb** with a digital, mobile-friendly voting application. A panel of **6 jurors** evaluates **11 teams** via their smartphones during the Prämierungsfeier on **19. Mai 2026, 18 Uhr** at **Science Park Kassel, Universitätsplatz 12** (hosted by Universität Kassel). Results are displayed in real time on a beamer/projector for the audience.
**Scoring system:** 5 equally-weighted criteria, each scored on a 110 scale (configurable). Maximum score per juror per team: 50. Aggregation: simple sum across all jurors, average = total / number of jurors.
**Visual design basis:** Projekt-KIQ-HP (React 19 + Vite) — dark theme (#0a0a0a background), neon green accents (#00FF00), glassmorphism panels, Outfit font, lucide-react icons. Premium event look.
**Tech stack:** React 19 + Vite, react-router-dom, lucide-react icons. NO Supabase — use a lightweight real-time backend (e.g., simple JSON server, or localStorage + polling for MVP since only 6 jurors).
**Reference documents (in repo):**
- `Jury-Voting/Teams/` — folder containing team/project descriptions (11 teams)
- `Jury-Voting/UNIKAT_IDEENWETTBEWERB_2026_Einladung_Prämierungsfeier.pdf` — event invitation
- `Jury-Voting/Bewertungsbogen Jury 2026.doc` — the paper scoring form (5 criteria, 110 scale)
- `Jury-Voting/Auswertungstabelle 2026.xlsx` — the paper results table (simple sum aggregation)
## Glossary
- **Voting_App**: The web application that jurors and the audience interact with
- **Juror**: A registered panel member who submits scores for each team (6 jurors this year: Cielejewski, Finke, Freyer, Knie, Meyer, Trieschmann)
- **Team**: A team/product/concept being evaluated (11 total in the current event)
- **Score**: A numeric rating (110) a juror assigns to a team for a given criterion
- **Criterion**: A dimension on which a team is evaluated (5 criteria: Gesamtidee, Kundennutzen, Markt, Realisierbarkeit, Präsentation)
- **Voting_Session**: A time-bounded event during which jurors submit their scores
- **Results_View**: The projector-optimized page that displays aggregated scores and rankings
- **Admin**: The person who configures the session, manages jurors, and controls the results display
## Concrete Event Data
### Jurors (6)
| # | Name |
|---|------|
| 1 | Cielejewski |
| 2 | Finke |
| 3 | Freyer |
| 4 | Knie |
| 5 | Meyer |
| 6 | Trieschmann |
### Teams (11)
| # | Team Name | Description | Contact |
|---|-----------|-------------|---------|
| 1 | Anima Lektor | Lektor for game language/text | Lukas Fleck |
| 2 | Das Any-Thing | Physical AI automaton that adapts to context | — |
| 3 | Doppelrad-Sortierer | AI-powered seed sorting machine for niche producers | — |
| 4 | DUAL | Fully reversible wood construction system | Nicole Kozlewski, Sebastian Görs |
| 5 | fömo | Funding management platform for social institutions | — |
| 6 | ISOmedi | Compact rechargeable medication case with protective micro-environment | — |
| 7 | Lian's Tempeh Chips | Tempeh-based snack product | Dr. Berlianti Puteri |
| 8 | Lumpen | Sustainable upholstery material from old clothing textiles | — |
| 9 | Mobile Sprinkleranlage | Mobile sprinkler system for fire protection | Björn Knoke |
| 10 | SafeSupply OS | Lightweight supplier risk & audit system for agri-food SMEs | Jesvin Jaksan |
| 11 | Second Nose | Digital smell detection module for smartphones | — |
### Scoring Criteria (5, equally weighted)
| # | Criterion (DE) | Criterion (EN) |
|---|----------------|----------------|
| 1 | Gesamtidee | Overall Idea |
| 2 | Kundennutzen | Customer Benefit |
| 3 | Markt | Market |
| 4 | Realisierbarkeit | Feasibility |
| 5 | Präsentation | Presentation |
**Scale:** 110 per criterion (configurable, default 110)
**Max per juror per team:** 50 points
**Aggregation:** Simple sum. Per-team total = sum of all juror scores. Average = total / 6.
### Presentation Schedule (partial, from Gesamtauswertung)
| Time | Team |
|------|------|
| 15:1015:30 | DUAL |
| 16:1016:30 | fömo |
| 16:3016:50 | Lumpen (inferred) |
| 16:5017:10 | Das Any-Thing |
| 17:1017:30 | Doppelrad-Sortierer |
| 17:4518:05 | Anima Lektor |
| 18:0518:25 | Mobile Sprinkleranlage |
## Requirements
### Requirement 1: Juror Authentication
**User Story:** As a juror, I want a simple way to identify myself when I open the app on my phone, so that my votes are attributed to me without a complex login flow.
#### Acceptance Criteria
1. WHEN a juror opens the Voting_App on a mobile device, THE Voting_App SHALL present a juror selection list showing the 6 registered juror names (Cielejewski, Finke, Freyer, Knie, Meyer, Trieschmann)
2. WHEN a juror selects their name from the list, THE Voting_App SHALL grant access to the voting interface within 3 seconds
3. IF a juror selects a name that is already in use by another active session, THEN THE Voting_App SHALL display a warning and allow override or retry
4. THE Voting_App SHALL support 6 concurrent jurors in a single Voting_Session
### Requirement 2: Mobile-Friendly Voting Interface
**User Story:** As a juror, I want to score each team on my smartphone quickly and intuitively, so that the evaluation process is faster than the paper form.
#### Acceptance Criteria
1. THE Voting_App SHALL render a responsive voting interface optimized for mobile viewports (320px428px width)
2. WHEN a juror navigates to a team, THE Voting_App SHALL display all 5 scoring criteria (Gesamtidee, Kundennutzen, Markt, Realisierbarkeit, Präsentation) with a 110 input for each
3. WHEN a juror selects a score for a criterion, THE Voting_App SHALL persist the score immediately without requiring a separate save action
4. THE Voting_App SHALL display all 11 teams in a navigable list with clear indication of which teams have been scored and which are pending
5. WHILE a juror is scoring a team, THE Voting_App SHALL display the team name and short description
### Requirement 3: Score Persistence and Conflict Handling
**User Story:** As a juror, I want my scores to be saved reliably even if my phone loses connection briefly, so that no votes are lost.
#### Acceptance Criteria
1. WHEN a juror submits a score, THE Voting_App SHALL persist the score to the server within 2 seconds under normal network conditions
2. IF the network connection is lost, THEN THE Voting_App SHALL store scores locally and synchronize them when connectivity is restored
3. WHEN a juror changes a previously submitted score, THE Voting_App SHALL overwrite the previous value with the new score
4. THE Voting_App SHALL prevent a juror from submitting scores for the same criterion on the same team more than once without explicit overwrite intent
### Requirement 4: Real-Time Results Aggregation
**User Story:** As an admin, I want the results to update in real time as jurors submit scores, so that the audience sees a live leaderboard on the projector.
#### Acceptance Criteria
1. WHEN a new score is persisted, THE Voting_App SHALL recalculate the aggregated result for the affected team within 1 second
2. THE Voting_App SHALL calculate a total score per team by summing all juror scores across all 5 criteria (simple sum, max possible = 6 jurors × 5 criteria × 10 points = 300)
3. THE Voting_App SHALL calculate an average score per team (total / number of jurors who have scored)
4. THE Voting_App SHALL rank teams from highest to lowest total score
5. IF two or more teams have the same total score, THEN THE Voting_App SHALL display them with equal rank
### Requirement 5: Beamer/Projector Results Display
**User Story:** As an event organizer, I want a dedicated results page optimized for projection on a large screen, so that the audience can follow the voting live.
#### Acceptance Criteria
1. THE Results_View SHALL be optimized for landscape display at 1920×1080 resolution
2. THE Results_View SHALL use the dark theme (#0a0a0a background) with neon green (#00FF00) accents, glassmorphism panels, and Outfit font for a premium event look
3. WHEN aggregated scores change, THE Results_View SHALL update the displayed rankings within 2 seconds without requiring a page refresh
4. THE Results_View SHALL display each team name, its total score, its average score, and its current rank
5. WHILE a Voting_Session is active, THE Results_View SHALL indicate that voting is in progress
6. WHEN a Voting_Session ends, THE Results_View SHALL display the final rankings with a clear "Final Results" indicator
### Requirement 6: Session Management
**User Story:** As an admin, I want to configure and control the voting session, so that I can set up teams, criteria, and start/stop voting at the right time.
#### Acceptance Criteria
1. THE Voting_App SHALL provide an admin interface to create and configure a Voting_Session
2. WHEN an admin creates a Voting_Session, THE Voting_App SHALL allow configuration of team names and descriptions (pre-populated with the 11 teams for this event)
3. WHEN an admin creates a Voting_Session, THE Voting_App SHALL allow configuration of scoring criteria and their scale (pre-populated with 5 criteria, 110 scale)
4. WHEN an admin starts a Voting_Session, THE Voting_App SHALL enable score submission for all 6 registered jurors
5. WHEN an admin ends a Voting_Session, THE Voting_App SHALL disable further score submission and mark results as final
6. THE Voting_App SHALL allow the admin to register jurors by name (pre-populated with the 6 jurors for this event)
### Requirement 7: Voting Progress Visibility
**User Story:** As an admin, I want to see which jurors have completed their scoring, so that I know when all votes are in and I can close the session.
#### Acceptance Criteria
1. THE Voting_App SHALL display a progress overview showing how many of the 11 teams each juror has fully scored (all 5 criteria submitted)
2. WHEN all 6 jurors have submitted scores for all 11 teams and all 5 criteria, THE Voting_App SHALL indicate that voting is complete
3. THE Voting_App SHALL allow the admin to view individual juror completion status without revealing their specific scores
### Requirement 8: Data Export
**User Story:** As an admin, I want to export the final results, so that I have a digital record comparable to the previous paper-based Auswertungstabelle.
#### Acceptance Criteria
1. WHEN a Voting_Session has ended, THE Voting_App SHALL provide an export of all scores in a tabular format (CSV or XLSX)
2. THE Voting_App SHALL include in the export: team names, juror names, individual scores per criterion (Gesamtidee, Kundennutzen, Markt, Realisierbarkeit, Präsentation), and aggregated totals
3. THE Voting_App SHALL include the final ranking and average scores in the export
4. THE Voting_App SHALL format the export to match the structure of the original Auswertungstabelle (teams as rows, jurors × criteria as columns)
---
## Open Questions (resolved)
| # | Question | Resolution |
|---|----------|------------|
| 1 | Scoring scale | **110 per criterion** (configurable, default 110) |
| 2 | Criteria | **5 criteria:** Gesamtidee, Kundennutzen, Markt, Realisierbarkeit, Präsentation |
| 3 | Weighting | **Equally weighted** — simple sum, no weighting factors |
| 4 | Aggregation | **Simple sum** across all jurors. Average = total / number of jurors |
| 5 | Juror anonymity | **TBD** — not specified in paper documents. Default: scores visible only to admin |
| 6 | Idea metadata | Team name + short description shown to jurors (from Teams/ folder) |
| 7 | Comments | **Not included** — paper form has no free-text field; digital version omits it too |
+294
View File
@@ -0,0 +1,294 @@
# 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 (110) 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
- [x] 1. Project scaffolding and configuration
- [x] 1.1 Initialize Vite + React 19 + TypeScript project in `Jury-Voting/`
- Run `npm create vite@latest` with React + TypeScript template
- Configure `vite.config.ts` with 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)_
- [x] 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.ts` entry point with basic Express app on port 3001
- Add `npm run dev:server` script using `tsx watch`
- _Requirements: 3.1 (score persistence)_
- [x] 1.3 Define shared TypeScript interfaces in `Jury-Voting/shared/types.ts`
- Create `Session`, `Team`, `Juror`, `Criterion`, `Score`, `TeamResult`, `PendingScore` interfaces
- Create `SessionConfig` interface with criteria and scale settings
- Export all types for use by both frontend and backend
- _Requirements: 6.2, 6.3 (configurable teams, criteria, scale)_
- [x] 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 110)
- _Requirements: 6.2, 6.3, 6.6_
- [x] 2. Backend data layer and core logic
- [x] 2.1 Implement JSON file store (`server/store.ts`)
- Read/write `server/data/session.json` atomically (write to temp, rename)
- Create `.bak` backup before each write
- Initialize from `seed.json` if `session.json` doesn't exist
- Expose typed read/write functions for session, teams, jurors, scores
- _Requirements: 3.1, 3.2 (persistence, reliability)_
- [x] 2.2 Implement score aggregation logic (`server/aggregation.ts`)
- `calculateTeamResult(teamId, scores[])` → total, average, jurorCount, complete
- `calculateRankings(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 (110) 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', 14 → 'partial', 0 → 'pending'
- **Validates: Requirements 2.4, 7.1**
- [x] 2.6 Implement last-write-wins conflict resolution (`server/conflict.ts`)
- Compare timestamps on incoming score vs stored score
- Accept score with later `updatedAt` timestamp
- 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
- [x] 3.1 Implement session endpoints
- `GET /api/session` — return current session state
- `POST /api/session/start` — set status to 'active', record startedAt
- `POST /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_
- [x] 3.2 Implement team and juror endpoints
- `GET /api/teams` — return all teams
- `GET /api/jurors` — return all jurors
- _Requirements: 6.2, 6.6_
- [x] 3.3 Implement score endpoints
- `POST /api/scores` — submit/update a score (validate: session active, score 110, valid jurorId/teamId/criterionId)
- `GET /api/scores/:jurorId` — return all scores for a juror
- `GET /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**
- [x] 3.5 Implement results and progress endpoints
- `GET /api/results` — return aggregated TeamResult[] with rankings
- `GET /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**
- [x] 4. SSE real-time infrastructure
- [x] 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_updated` event 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_
- [x] 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)_
- [x] 5. Checkpoint - Backend complete
- Ensure all tests pass, ask the user if questions arise.
- [x] 6. Frontend: Juror selection and routing
- [x] 6.1 Set up React Router with route structure
- Configure routes: `/`, `/vote`, `/vote/:teamId`, `/results`, `/admin`
- Create `App.tsx` with `BrowserRouter` and route definitions
- Create layout wrapper with dark theme base styles
- _Requirements: 1.1, 2.1_
- [x] 6.2 Implement `JurorSelect` page (`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_
- [ ]* 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
- [x] 7.1 Implement `VotingDashboard` page (`src/pages/VotingDashboard.tsx`)
- Fetch teams from `GET /api/teams` and scores from `GET /api/scores/:jurorId`
- Display 11 `TeamCard` components 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_
- [x] 7.2 Implement `TeamScoring` page (`src/pages/TeamScoring.tsx`)
- Display team name and description at top
- Render 5 `ScoreSlider` components (one per criterion)
- Each slider: range 110, 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_
- [x] 7.3 Implement offline score queue (`src/hooks/useOfflineQueue.ts`)
- On score change: save to localStorage `pendingScores` array 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_
- [ ]* 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)
- [x] 8.1 Implement `ResultsView` page (`src/pages/ResultsView.tsx`)
- Full-screen layout, no navigation chrome
- Connect to SSE via `useSSE` hook
- Display `RankingTable` component 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_
- [x] 8.2 Implement `RankingTable` component (`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
- [x] 9.1 Implement `AdminPanel` page (`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_
- [x] 9.2 Implement `ProgressMatrix` component (`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_
- [x] 9.3 Implement CSV export functionality
- Export button calls `GET /api/export/csv` and 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_
- [ ]* 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**
- [x] 10. Checkpoint - Core features complete
- Ensure all tests pass, ask the user if questions arise.
- [ ] 11. Styling and visual polish
- [x] 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_
- [x] 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 (320px428px primary, scales up)
- _Requirements: 2.1, 5.1, 5.2_
- [ ] 12. Deployment configuration
- [x] 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 build` and `npm start` scripts
- _Requirements: all (deployment readiness)_
- [x] 12.2 Create `docker-compose.yml` for 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 `.dockerignore` and `Dockerfile`
- _Requirements: all (deployment readiness)_
- [x] 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