Add a results archive and purge orphaned passing rows on delete
Until now there was no way to find a completed session without already knowing which event it belonged to - navigation was strictly Events -> one event -> its sessions. A new "Resultatarkiv" view lists every finished session across all events, searchable by event/session name, class, driver and date range. Opening a session reuses the existing single-session leaderboard builder for standings and the existing per-competitor lap lookup for lap history - no new results logic - plus a session-wide penalties/ corrections list (resultsBySession[id].adjustments), previously only visible in the live judging action log. Export buttons reuse the existing per-event PDF and race-package JSON export functions rather than adding a new format. Investigated what should happen to raw timing data when a race is deleted. Training statistics and session replay already read from their own independent, deliberately-retained copies that survive deletion by existing design - unaffected either way. The one thing that outlived deletion with no purpose was the raw `passings` SQLite rows (write-only; nothing in the app reads them), growing forever with driver IDs never purged. A new POST /api/passings/purge endpoint now removes exactly the deleted session(s)' rows, called as a best-effort side call from both delete-event and delete-session. Also fixes a real bug found while building this: ensureSessionResult guarded adjustments/splits/pitEvents/pitStops against being missing on an already-existing result, but not passings itself, so viewing lap history for such a result (e.g. an older/imported race package) crashed outright. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
100
src/app.js
100
src/app.js
@@ -26,6 +26,7 @@ import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFro
|
||||
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
|
||||
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildDriverTimingChannelStats as buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildEvenLapsProgress as buildEvenLapsProgressLogic, buildStepTrainingProgress as buildStepTrainingProgressLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic, buildTrainingMissions as buildTrainingMissionsLogic, buildTrainingHighlights as buildTrainingHighlightsLogic, getClubTrainingGoalProgress as getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets as buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats as buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis as buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons as buildTrainingMarkerComparisonsLogic } from "./training_logic.js";
|
||||
import { renderTrainingView } from "./training_views.js";
|
||||
import { renderResultsArchiveView } from "./results_archive_view.js";
|
||||
import { importDriversCsv, importCarsCsv } from "./csv_import.js";
|
||||
|
||||
|
||||
@@ -325,6 +326,13 @@ const buildStepTrainingProgress = (trainingState, driverId, classId, config, now
|
||||
const buildTrainingRecommendation = (stats) => buildTrainingRecommendationLogic(stats);
|
||||
const getTrainingGoalProgress = (goal, stats) => getTrainingGoalProgressLogic(goal, stats);
|
||||
const getNextTrainingMilestone = (stats) => getNextTrainingMilestoneLogic(stats);
|
||||
const renderArchive = () => renderResultsArchiveView({
|
||||
state, dom, t, escapeHtml, formatLap, formatRaceClock, renderTable,
|
||||
getClassName, getEventName, buildLeaderboard, getCompetitorPassings,
|
||||
exportRaceResultsPdf, buildRacePackagePayload, downloadJsonFile, sanitizeFilenameSegment,
|
||||
getArchiveFilters: () => archiveFilters, setArchiveFilters: (value) => { archiveFilters = value; },
|
||||
getSelectedSessionId: () => archiveSelectedSessionId, setSelectedSessionId: (value) => { archiveSelectedSessionId = value; },
|
||||
});
|
||||
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, formatRaceClock, uid, saveState, syncStateToBackend, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildDriverTimingChannelStats: buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats, buildEvenLapsProgress, buildStepTrainingProgress, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, buildTrainingMissions: buildTrainingMissionsLogic, buildTrainingHighlights: buildTrainingHighlightsLogic, getClubTrainingGoalProgress: getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets: buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats: buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis: buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons: buildTrainingMarkerComparisonsLogic, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", value, selectedTrainingClassId); }, getSelectedClassId: () => selectedTrainingClassId, setSelectedClassId: (value) => { selectedTrainingClassId = value; updateViewUrl("training", selectedTrainingDriverId, value); } });
|
||||
const applyCompetitorCorrection = (session, row, options = {}) => {
|
||||
const result = applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
|
||||
@@ -350,6 +358,7 @@ const NAV_ITEMS = [
|
||||
{ id: "judging", titleKey: "nav.judging", subtitleKey: "nav.judging_sub", groupKey: "nav.group_run" },
|
||||
{ id: "overlay", titleKey: "nav.overlay", subtitleKey: "nav.overlay_sub", groupKey: "nav.group_run" },
|
||||
{ id: "training", titleKey: "nav.training", subtitleKey: "nav.training_sub", groupKey: "nav.group_run" },
|
||||
{ id: "archive", titleKey: "nav.archive", subtitleKey: "nav.archive_sub", groupKey: "nav.group_run" },
|
||||
{ id: "classes", titleKey: "nav.classes", subtitleKey: "nav.classes_sub", groupKey: "nav.group_register" },
|
||||
{ id: "drivers", titleKey: "nav.drivers", subtitleKey: "nav.drivers_sub", groupKey: "nav.group_register" },
|
||||
{ id: "cars", titleKey: "nav.cars", subtitleKey: "nav.cars_sub", groupKey: "nav.group_register" },
|
||||
@@ -448,6 +457,39 @@ const TRANSLATIONS = {
|
||||
"nav.cars_sub": "Bilar med fasta transpondrar",
|
||||
"nav.training": "Träningsstatistik",
|
||||
"nav.training_sub": "Utveckling, delmål och belöningar per förare",
|
||||
"nav.archive": "Resultatarkiv",
|
||||
"nav.archive_sub": "Sök avslutade race utan att gå via Race Setup",
|
||||
"archive.title": "Resultatarkiv",
|
||||
"archive.hint": "Sök bland alla avslutade sessioner efter datum, klass eller förare.",
|
||||
"archive.search": "Sök",
|
||||
"archive.search_placeholder": "Event- eller sessionsnamn",
|
||||
"archive.class": "Klass",
|
||||
"archive.all_classes": "Alla klasser",
|
||||
"archive.driver": "Förare",
|
||||
"archive.all_drivers": "Alla förare",
|
||||
"archive.date_from": "Från datum",
|
||||
"archive.date_to": "Till datum",
|
||||
"archive.date": "Datum",
|
||||
"archive.event": "Event",
|
||||
"archive.session": "Session",
|
||||
"archive.drivers": "Förare",
|
||||
"archive.winner": "Vinnare",
|
||||
"archive.penalties_short": "Straff",
|
||||
"archive.view": "Visa",
|
||||
"archive.no_results": "Inga avslutade sessioner matchar filtren.",
|
||||
"archive.standings": "Resultat",
|
||||
"archive.no_standings": "Inga resultat registrerade.",
|
||||
"archive.penalties": "Straff och korrigeringar",
|
||||
"archive.action": "Åtgärd",
|
||||
"archive.detail": "Detalj",
|
||||
"archive.no_penalties": "Inga straff eller korrigeringar registrerade för den här sessionen.",
|
||||
"archive.adjustment_correction": "Korrigering",
|
||||
"archive.adjustment_invalid": "Ogiltigförklaring",
|
||||
"archive.adjustment_undo": "Ångrad",
|
||||
"archive.lap_history": "Varvhistorik per förare",
|
||||
"archive.close": "Stäng",
|
||||
"archive.export_pdf": "Exportera PDF",
|
||||
"archive.export_json": "Exportera racepaket (JSON)",
|
||||
"nav.timing": "Tidtagning",
|
||||
"nav.timing_sub": "Live timing-board",
|
||||
"nav.judging": "Domare",
|
||||
@@ -1862,6 +1904,39 @@ const TRANSLATIONS = {
|
||||
"nav.cars_sub": "Track cars with fixed transponders",
|
||||
"nav.training": "Training stats",
|
||||
"nav.training_sub": "Development, milestones and rewards per driver",
|
||||
"nav.archive": "Results archive",
|
||||
"nav.archive_sub": "Search completed races without going through Race Setup",
|
||||
"archive.title": "Results archive",
|
||||
"archive.hint": "Search all completed sessions by date, class or driver.",
|
||||
"archive.search": "Search",
|
||||
"archive.search_placeholder": "Event or session name",
|
||||
"archive.class": "Class",
|
||||
"archive.all_classes": "All classes",
|
||||
"archive.driver": "Driver",
|
||||
"archive.all_drivers": "All drivers",
|
||||
"archive.date_from": "From date",
|
||||
"archive.date_to": "To date",
|
||||
"archive.date": "Date",
|
||||
"archive.event": "Event",
|
||||
"archive.session": "Session",
|
||||
"archive.drivers": "Drivers",
|
||||
"archive.winner": "Winner",
|
||||
"archive.penalties_short": "Penalties",
|
||||
"archive.view": "View",
|
||||
"archive.no_results": "No completed sessions match the filters.",
|
||||
"archive.standings": "Results",
|
||||
"archive.no_standings": "No results recorded.",
|
||||
"archive.penalties": "Penalties and corrections",
|
||||
"archive.action": "Action",
|
||||
"archive.detail": "Detail",
|
||||
"archive.no_penalties": "No penalties or corrections recorded for this session.",
|
||||
"archive.adjustment_correction": "Correction",
|
||||
"archive.adjustment_invalid": "Invalidation",
|
||||
"archive.adjustment_undo": "Undone",
|
||||
"archive.lap_history": "Lap history per driver",
|
||||
"archive.close": "Close",
|
||||
"archive.export_pdf": "Export PDF",
|
||||
"archive.export_json": "Export race package (JSON)",
|
||||
"nav.timing": "Timing",
|
||||
"nav.timing_sub": "Live timing board",
|
||||
"nav.judging": "Judging",
|
||||
@@ -3412,6 +3487,8 @@ function writeObsOverlayParams(url, config) {
|
||||
}
|
||||
let raceWizardStep = 1;
|
||||
let raceWizardDraft = null;
|
||||
let archiveFilters = { search: "", classId: "", driverId: "", dateFrom: "", dateTo: "" };
|
||||
let archiveSelectedSessionId = "";
|
||||
let overlaySyncTimer = null;
|
||||
let overlayRotationTimer = null;
|
||||
let overlayLiveRefreshTimer = null;
|
||||
@@ -4379,6 +4456,24 @@ async function createServerBackup(reason = "manual", session = null) {
|
||||
return payload.backup || null;
|
||||
}
|
||||
|
||||
// The raw passing rows behind a deleted event/session live only in SQLite, outside
|
||||
// buildPersistableState()/POST /api/state entirely, so deleting the event/session locally
|
||||
// can't reach them - this is a best-effort side call to ask the server to clean them up too.
|
||||
// A failure here (offline, etc.) is not worth blocking or rolling back the deletion itself.
|
||||
async function purgeSessionPassings(sessionIds) {
|
||||
const ids = (Array.isArray(sessionIds) ? sessionIds : [sessionIds]).filter(Boolean);
|
||||
if (!ids.length) return;
|
||||
try {
|
||||
await fetch(`${getBackendUrl()}/api/passings/purge`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionIds: ids }),
|
||||
});
|
||||
} catch {
|
||||
// best-effort cleanup only
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToView(view) {
|
||||
currentView = view;
|
||||
updateViewUrl(view);
|
||||
@@ -4482,6 +4577,9 @@ function renderView() {
|
||||
case "training":
|
||||
renderTraining();
|
||||
break;
|
||||
case "archive":
|
||||
renderArchive();
|
||||
break;
|
||||
case "timing":
|
||||
renderTiming();
|
||||
break;
|
||||
@@ -4679,6 +4777,7 @@ function renderEventWorkspace(mode) {
|
||||
bindModalShell,
|
||||
isValidIsoDate,
|
||||
saveState,
|
||||
purgeSessionPassings,
|
||||
openTimingSession: (sessionId) => {
|
||||
state.activeSessionId = sessionId;
|
||||
saveState();
|
||||
@@ -4740,6 +4839,7 @@ function renderEventManager(eventId) {
|
||||
bindModalShell,
|
||||
setFormError,
|
||||
saveState,
|
||||
purgeSessionPassings,
|
||||
renderView,
|
||||
rerenderEventManager: renderEventManager,
|
||||
updateHeaderState,
|
||||
|
||||
Reference in New Issue
Block a user