Decision Log¶
Key architectural decisions and their rationale.
D1 — Distance as the primary axis for lap comparison¶
Decision: All lap comparison and delta calculation uses track distance (metres) as the x-axis, not time.
Why: Two laps at different pace have the same physical track positions but different timestamps. Time-based alignment would smear corner positions across the comparison. Distance-based alignment preserves spatial meaning — "this car is 50 metres before braking" is a stable statement regardless of pace.
Implementation: LapResampler interpolates both laps to N=512 evenly-spaced distance points.
D2 — Raw telemetry stays on the desktop¶
Decision: Full telemetry point arrays (TelemetryPoint[]) are never uploaded to the API. Only session summaries (lap times, quality scores, track name) travel to the cloud.
Why: Privacy, bandwidth, and storage cost. A typical session is 10–50 MB of CSV data. Uploading it for every sync would be prohibitive and unnecessary — all analysis runs client-side.
Implementation: SessionUploadDto contains only LapSummaryDto[] + metadata. The TrackPoints field is optional and only used for centerline processing contribution.
D3 — Transactional outbox for side effects¶
Decision: Feed entries and achievement checks are triggered via an outbox_messages table written in the same DB transaction as the session/leaderboard insert.
Why: Without the outbox, a process crash between the business INSERT and the downstream service call would silently drop the side effect. The outbox guarantees at-least-once delivery.
Implementation: OutboxDispatcher polls every 5 seconds, retries up to 5 times.
D4 — Single-entry leaderboard per (track, user)¶
Decision: The leaderboard stores one row per (track_id, user_id) — the user's personal best. New submissions replace the row if faster; slower submissions are discarded.
Why: Leaderboards measure peak performance, not volume. Multiple entries per user would require a separate "personal best" aggregation on every read.
Implementation: UNIQUE(track_id, user_id) constraint + ON CONFLICT DO UPDATE IF new_time < existing_time logic in LeaderboardsController.
D5 — Blazor Server for SafeWatch (not a separate SPA)¶
Decision: The SafeWatch admin dashboard is a Blazor Server app mounted inside SimCopilot.Api at /safewatch, not a separate React/Angular SPA.
Why: SafeWatch is an internal tool with a small number of concurrent users. Blazor Server allows direct AppDbContext access inside components — no new API endpoints needed. Deploying inside the existing API avoids a second service.
Implementation: MapRazorComponents<App>().AddInteractiveServerRenderMode() alongside MapControllers().
D6 — Config-based auth for SafeWatch (not database accounts)¶
Decision: SafeWatch user accounts are stored in appsettings.json with BCrypt-hashed passwords. No database table for admin users.
Why: SafeWatch has a fixed, small number of accounts (4 roles, managed by the team). A full user management system would be over-engineering. Config-based accounts are auditable via git history.
Implementation: SafeWatch:Accounts array in appsettings.json, validated by SafeWatchLoginService.