feat: stable teamId in store + shared collective DTOs (B-prep)
- session store gains a persisted teamId (crypto.randomUUID, preserved across reset) used as the team's stable identity for the collective - src/types.ts: TeamSnapshot/SubmissionDTO/ScoreDTO/LeaderboardRow/WsEvent, the contract shared with the api backend 2 new tests; suite 88/88 green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7c141dcd89
commit
ca023cdaa5
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { useSession } from './session'
|
||||||
|
|
||||||
|
describe('session store · teamId', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a non-empty stable team id', () => {
|
||||||
|
const id = useSession.getState().teamId
|
||||||
|
expect(id).toBeTruthy()
|
||||||
|
expect(typeof id).toBe('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves the team id across reset()', () => {
|
||||||
|
const before = useSession.getState().teamId
|
||||||
|
useSession.getState().setTeam({ name: 'x' })
|
||||||
|
useSession.getState().reset()
|
||||||
|
expect(useSession.getState().teamId).toBe(before)
|
||||||
|
expect(useSession.getState().team.name).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -47,7 +47,14 @@ export interface Submission {
|
|||||||
submittedAt: string | null
|
submittedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stable per-browser identity, generated once and persisted. */
|
||||||
|
function genTeamId(): string {
|
||||||
|
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID()
|
||||||
|
return `team-${Math.abs(Date.now() ^ (Math.floor(Math.random() * 1e9))).toString(36)}`
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionState {
|
export interface SessionState {
|
||||||
|
teamId: string
|
||||||
team: Team
|
team: Team
|
||||||
device: Device
|
device: Device
|
||||||
phases: Record<PhaseKey, boolean>
|
phases: Record<PhaseKey, boolean>
|
||||||
@@ -66,6 +73,7 @@ export interface SessionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initial = {
|
const initial = {
|
||||||
|
teamId: genTeamId(),
|
||||||
team: { name: '', members: [] as string[], kit: 'KIT-01' },
|
team: { name: '', members: [] as string[], kit: 'KIT-01' },
|
||||||
device: { connected: false, port: null, uptimeS: 0 },
|
device: { connected: false, port: null, uptimeS: 0 },
|
||||||
phases: { reg: false, setup: false, m1: false, m2: false, add: false } as Record<PhaseKey, boolean>,
|
phases: { reg: false, setup: false, m1: false, m2: false, add: false } as Record<PhaseKey, boolean>,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Shared DTOs for the collective backend. Mirrored by api/src/types.ts —
|
||||||
|
// keep the two in sync by hand (low churn, single-day event).
|
||||||
|
import type { PhaseKey, SessionStats, AddLayers } from '@/store/session'
|
||||||
|
|
||||||
|
/** A team's projection pushed up to the collective. */
|
||||||
|
export interface TeamSnapshot {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
kit: string
|
||||||
|
members: string[]
|
||||||
|
phases: Record<PhaseKey, boolean>
|
||||||
|
stats: SessionStats
|
||||||
|
deviceConnected: boolean
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmissionDTO {
|
||||||
|
teamId: string
|
||||||
|
teamName: string
|
||||||
|
code: string
|
||||||
|
add: AddLayers
|
||||||
|
submittedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lightweight queue row for the judge (no full ADD payload). */
|
||||||
|
export interface SubmissionSummary {
|
||||||
|
teamId: string
|
||||||
|
teamName: string
|
||||||
|
submittedAt: string
|
||||||
|
scored: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreInput {
|
||||||
|
teamId: string
|
||||||
|
judge: string
|
||||||
|
rubric: Record<string, number>
|
||||||
|
total: number
|
||||||
|
notes: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreDTO extends ScoreInput {
|
||||||
|
id: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeaderboardRow {
|
||||||
|
teamId: string
|
||||||
|
teamName: string
|
||||||
|
avgTotal: number
|
||||||
|
scoreCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebSocket events broadcast by the hub to admin/judge clients. */
|
||||||
|
export type WsEvent =
|
||||||
|
| { type: 'snapshot'; teams: TeamSnapshot[]; submissions: SubmissionSummary[] }
|
||||||
|
| { type: 'team:update'; team: TeamSnapshot }
|
||||||
|
| { type: 'submission:new'; submission: SubmissionSummary }
|
||||||
|
| { type: 'score:new'; teamId: string; total: number }
|
||||||
Reference in New Issue
Block a user