OpenVolleyScout uses a mix of persisted aggregates, browser-local editor state, and derived runtime state.
The important rule is that match data should be reconstructable from the
persisted MatchProject aggregate and its event log.
Defined in src/domain/match/types.ts.
MatchProject is the top-level persisted match aggregate. It contains:
metadata- competition, date, venue, match number, format, and schema version.homeSelectionandawaySelection- canonical match-specific team data.homeTeamandawayTeam- derived read models for UI consumers.phase- current lifecycle state such asstartup,scouting,analysis, orclosed.events- orderedMatchEventrecords.scoutingConfig- set target points, tie-break target, max sets to win, and golden-set flags.scoutingSession- the latest persisted scouting session snapshot.- linked tactical metadata arrays reserved for future integrations.
videoAnalysis- optional video source reference, synchronization points, and clip-padding preferences. The video file itself is never stored in the project.- timestamps.
The canonical team data lives in the side selections. The derived homeTeam
and awayTeam snapshots are rebuilt by normalizeMatchProject().
Defined in src/domain/team/types.ts.
ArchivedTeam stores team identity, staff, team code, roster references, and
timestamps.
ArchivedRoster stores the actual archived player list for a team. The current
UI mostly edits the latest roster, but the model supports historical roster
records.
Defined in src/domain/archive/types.ts.
Competition archive entries store reusable competition names for match setup suggestions.
The current Systems page edits:
DefenseSystemBlockReceptionSystemBlock
Both are defined in src/domain/systems/types.ts and persisted as editor
libraries in localStorage.
This is separate from the more generic TacticalSystemDefinition model, which
is prepared for position-based zone responsibility editing but is not the main
editor state used by the current Systems page.
Defined in src/domain/video/types.ts.
MatchVideoAnalysis stores:
source- either a local file reference or a YouTube URL/video id.syncPoints- anchors that map event-clock seconds to video seconds.paddingBeforeSecondsandpaddingAfterSeconds- review/export window padding.updatedAt.
The model intentionally stores references only. Local browser file handles are stored outside the match project in a separate IndexedDB database because they cannot go through normal JSON serialization.
useAppStore stores the currently loaded or newly created MatchProject.
Setting an active project clones and normalizes it.
LiveMatchState extends ScoutingSession with an eventLog.
It is managed by useScoutingStore and derived by replaying match events. The
store does not treat nested session fields as the source of truth. Events are
the primary source, and session fields are replay results used by the UI.
Several pages also keep short-lived form or interaction state:
- match setup wizard form data
- team editor form state
- selected live-court zone
- score-correction dialog state
- system editor draft positions
- analysis/video filters and selected action state
Draft state should be saved through repositories or feature stores before it is treated as durable.
Defined in src/domain/events/types.ts.
Current event variants:
match_createdset_startedrally_startedtouch_recordedpoint_awardedsubstitution_madetimeout_calledlibero_replacement_madered_card_pointreplay_actionvideo_check_correctionsanction_recordeddead_ball_event_recordedsetter_assignedset_endedrally_ended
The active replay logic currently supports:
set_startedrally_startedtouch_recordedpoint_awardedlibero_replacement_madered_card_pointreplay_actionvideo_check_correctionsanction_recordeddead_ball_event_recordedsetter_assignedset_endedrally_ended
Defined in src/domain/touch/types.ts.
BallTouch records:
- set and rally number
- sequence number
- team side
- optional player id
- skill and evaluation
- zone, origin-zone, and target-zone references
- timestamp
Touches are embedded in touch_recorded events and are used by the statistics
builder and DataVolley-like code generation.
Defined in src/domain/scouting/types.ts.
ScoutingMatchConfig controls match progression:
- match format
- max sets to win
- regular set target points
- tie-break target points
- golden-set flags
ScoutingSession stores the latest replayed session snapshot:
- active project id
- current set and rally numbers
- score
- serving team
- active lineups
- rally activity flags
- current rally touches
- completed sets
- timestamps
useScoutingPersistence writes this snapshot back to the active project after
live scouting changes.
The statistics builder lives in src/features/scouting/model/match-stats.ts.
It can derive:
- team skill totals
- player skill totals
- quick stats for serve, reception, attack, and block
- rally summaries
- set summaries
- side-out and break-point stats
- rotation stats
- DataVolley-like rally codes
These statistics are rendered by scouting summary stages, AnalysisPage, and
TeamAnalysisPage. They are validated by scripts/validate-match-stats.mjs
and by the broader npm test suite.
src/features/teams/model/aggregated-stats.ts adapts several MatchStats
objects into one aggregate MatchStats for team-level study.
The selected team is normalized to home; all opponents are normalized to
away. This lets team performance, player performance, side-out, heatmap, and
video workflows reuse the same dashboard contracts. Aggregated set stats are
synthetic buckets by set number rather than real match sets.
normalizeMatchProject() is the key compatibility boundary. It:
- upgrades legacy schema versions to at least schema version 3
- normalizes side selections
- rebuilds derived team snapshots
- normalizes scouting config fields
- creates a default
scoutingSessionwhen missing - preserves optional
videoAnalysismetadata - ensures linked tactical arrays exist
New persistence or import paths should pass match projects through this normalization boundary.