diff --git a/README.md b/README.md index b8d21bb..466ba9c 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Recommended deployment: - Race Day operation: - focused current/next/after-next heat queue - preflight for decoder, participants, transponders, duplicate IDs, lap window, grid and audio + - blockers and warnings shown first, with shortcuts to the relevant settings or register; lap limits can be edited directly in Race Day - shared start/finish/pause logic with the existing Timing view - automatic consistent SQLite backup before every session start, full data import and data clear operation - latest 30 server backups listed under `Settings -> Storage` diff --git a/README.sv.md b/README.sv.md index bdcebda..2791cc1 100644 --- a/README.sv.md +++ b/README.sv.md @@ -115,6 +115,7 @@ Rekommenderad deploy: - Grupperad meny följer arbetsordningen: `Start`, `Skapa`, `Kör och följ`, `Register` samt `System och hjälp`. - `Översikt -> Kom igång` länkar direkt till systeminställningar, förarregister, race/practice, Tidtagning och Overlay. - Race Day-drift: + - förkontroll visar blockerare och varningar först med genvägar till rätt inställningar eller register; min/max-varvtid kan ändras direkt i Race Day - fokuserad heat-kö med Pågår, Nästa och Därefter - förkontroll av decoder, deltagare, transpondrar, dubbla ID, varvfönster, grid och ljud - samma start-, stopp- och pauslogik som den befintliga Tidtagning-vyn diff --git a/src/app.js b/src/app.js index 2cca74a..f6253bb 100644 --- a/src/app.js +++ b/src/app.js @@ -375,6 +375,7 @@ const TRANSLATIONS = { "race_day.start_list": "Startlista", "race_day.no_start_list": "Ingen startlista finns för sessionen.", "race_day.preflight": "Förkontroll", + "race_day.check_summary": "{blockers} blockerar start · {warnings} varningar", "race_day.heat_queue": "Heat-kö", "race_day.select": "Välj", "race_day.open_practice_any": "Alla inkommande transpondrar tillåts", @@ -1595,7 +1596,7 @@ const TRANSLATIONS = { "guide.card_race_day_blurb": "Förkontroll, startlista, heat-kö, automatisk backup och fokuserad racedrift.", "guide.race_day_1": "1. Skapa race och sessioner i Race Setup. Öppna sedan Race Day från gruppen Start.", "guide.race_day_2": "2. Välj aktuellt heat. Pågår, Nästa och Därefter följer sessionernas ordning i samma event.", - "guide.race_day_3": "3. Förkontrollen blockerar start när Main decoder är offline, deltagare eller transpondrar saknas, transpondrar är dubbla, varvfönstret är ogiltigt eller en positionsgrid är ofullständig.", + "guide.race_day_3": "3. Förkontrollen visar blockerare först, sedan varningar och godkända kontroller. Knapparna vid problemen öppnar relevant meny; min/max kan rättas direkt i Race Day. Förkontrollen blockerar start när Main decoder är offline, deltagare eller transpondrar saknas, transpondrar är dubbla, varvfönstret är ogiltigt eller en positionsgrid är ofullständig.", "guide.race_day_4": "4. Gula kontroller är varningar. Valfria split-/depådecoders, backend och ljud visas tydligt men ändrar inte vanlig varvräkning.", "guide.race_day_5": "5. Vid start synkas state till backend och en konsistent SQLite-backup skapas i data/backups innan den befintliga audio- och starttonssekvensen körs. De 30 senaste sparas.", "guide.race_day_6": "6. Använd Race Day för start, paus, fortsätt och avslut. Tidtagning, Domare och Overlay finns som direkta genvägar. Lokal restore kräver stoppad server och npm run backup:restore -- --confirm.", @@ -1748,6 +1749,7 @@ const TRANSLATIONS = { "race_day.start_list": "Start list", "race_day.no_start_list": "No start list is available for this session.", "race_day.preflight": "Preflight", + "race_day.check_summary": "{blockers} blocking start · {warnings} warnings", "race_day.heat_queue": "Heat queue", "race_day.select": "Select", "race_day.open_practice_any": "All incoming transponders are allowed", @@ -2968,7 +2970,7 @@ const TRANSLATIONS = { "guide.card_race_day_blurb": "Preflight, start list, heat queue, automatic backup, and focused race operation.", "guide.race_day_1": "1. Create the race and sessions in Race Setup, then open Race Day from the Start group.", "guide.race_day_2": "2. Select the current heat. Running, Next, and After next follow session order within the same event.", - "guide.race_day_3": "3. Preflight blocks start when the Main decoder is offline, participants or transponders are missing, transponders are duplicated, the lap window is invalid, or a position grid is incomplete.", + "guide.race_day_3": "3. Preflight lists blockers first, then warnings and passed checks. Buttons beside issues open the relevant menu; min/max can be corrected directly in Race Day. Preflight blocks start when the Main decoder is offline, participants or transponders are missing, transponders are duplicated, the lap window is invalid, or a position grid is incomplete.", "guide.race_day_4": "4. Yellow checks are warnings. Optional split/pit decoders, backend, and audio are visible but do not change normal lap counting.", "guide.race_day_5": "5. Starting synchronizes state to the backend and creates a consistent SQLite backup in data/backups before the existing audio and start-tone sequence runs. The latest 30 are retained.", "guide.race_day_6": "6. Use Race Day for start, pause, resume, and finish. Timing, Judging, and Overlay remain one click away. Local restore requires a stopped server and npm run backup:restore -- --confirm.", diff --git a/src/race_day_view.js b/src/race_day_view.js index cd8c64a..855b860 100644 --- a/src/race_day_view.js +++ b/src/race_day_view.js @@ -1,3 +1,28 @@ +export function getRaceDayCheckAction(item, session) { + if (item.level === "ok") return null; + if (["backend", "decoder", "split_decoder", "pit_decoders", "audio"].includes(item.id)) { + return { view: "settings", label: "nav.settings" }; + } + if (item.id === "lap_window" && session?.mode === "race") { + return { field: "minLapSec", label: "race_day.lap_window_edit" }; + } + if (item.id === "session") return { view: "timing", label: "nav.timing" }; + if (["transponders", "duplicate_transponders"].includes(item.id) && session?.type !== "team_race") { + const view = session?.mode === "track" ? "cars" : "drivers"; + return { view, label: "nav." + view }; + } + if (["participants", "grid", "transponders", "duplicate_transponders", "lap_window"].includes(item.id)) { + const view = session?.mode === "track" ? "events" : "race_setup"; + return { view, label: "nav." + view }; + } + return null; +} + +export function sortRaceDayChecks(checks) { + const priority = { error: 0, warning: 1, ok: 2 }; + return [...checks].sort((a, b) => priority[a.level] - priority[b.level]); +} + function renderPreflightDetail(item, { t, formatLap, escapeHtml }) { if (item.id === "lap_window") return `${formatLap(item.minLapMs || 0)} / ${formatLap(item.maxLapMs || 0)}`; if (item.id === "participants") return item.openPractice ? t("race_day.open_practice_any") : t("race_day.count", { count: item.count || 0 }); @@ -35,6 +60,9 @@ export function renderRaceDayView(deps) { const queue = buildRaceDayQueue(state); const session = queue.current; const preflight = buildRaceDayPreflight(state, session, { backendAvailable }); + const checks = sortRaceDayChecks(preflight.checks); + const blockers = checks.filter((item) => item.level === "error").length; + const warnings = checks.filter((item) => item.level === "warning").length; const lapWindowCheck = preflight.checks.find((item) => item.id === "lap_window") || null; const timing = session ? getSessionTiming(session) : null; const clock = !timing ? "-" : timing.followUpActive @@ -91,9 +119,12 @@ export function renderRaceDayView(deps) {