feat(judging): keep penalties and race lap changes in the Judging view only
Race sessions: Timing driver details become read-only with an Open in Judging button; penalties and lap changes happen only in Judging. Practice: no penalties anywhere, per-lap clean-up stays on Timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
18
src/app.js
18
src/app.js
@@ -465,7 +465,7 @@ const refreshTimingDynamics = () => {
|
||||
node.textContent = clock;
|
||||
});
|
||||
};
|
||||
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, renderSessionChannelAnalysis, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, startOperationalSession, finishOperationalSession, cancelRaceStart, triggerRaceAudio, getStartLightState: () => raceAudioEngine.getStartLightState(), saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; }, sessionReplay, syncTrainingLedger, setCompetitorLapStatus });
|
||||
const renderTiming = () => renderTimingView({ state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, formatRaceClock, renderTable, uid, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings, getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid, renderSessionChannelAnalysis, connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, startOperationalSession, finishOperationalSession, cancelRaceStart, triggerRaceAudio, getStartLightState: () => raceAudioEngine.getStartLightState(), saveState, updateHeaderState, renderView, normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey: () => selectedLeaderboardKey, setSelectedLeaderboardKey: (value) => { selectedLeaderboardKey = value; }, getQuickAddDraft: () => quickAddDraft, setQuickAddDraft: (value) => { quickAddDraft = value; }, getPassingValidationLabel, isCountedPassing, getCompetitorPassings, getManualCorrectionSummary, formatTeamActiveMemberLabel, lastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents: () => overlayEvents, setOverlayEvents: (value) => { overlayEvents = value; }, sessionReplay, syncTrainingLedger, setCompetitorLapStatus, openJudging: (rowKey) => { selectedJudgeKey = rowKey; selectedLeaderboardKey = null; navigateToView("judging"); } });
|
||||
const renderJudging = () => renderJudgingView({ state, dom, t, escapeHtml, formatLap, renderTable, getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTypeLabel, getJudgeFilteredRows, getJudgeFilteredLog, getCompetitorPassings, isCountedPassing, getPassingValidationLabel, undoJudgingAdjustment, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap, renderView, getSelectedJudgeKey: () => selectedJudgeKey, setSelectedJudgeKey: (value) => { selectedJudgeKey = value; }, getJudgingCompetitorFilter: () => judgingCompetitorFilter, setJudgingCompetitorFilter: (value) => { judgingCompetitorFilter = value; }, getJudgingLogFilter: () => judgingLogFilter, setJudgingLogFilter: (value) => { judgingLogFilter = value; } , setCompetitorLapStatus });
|
||||
const normalizeTrainingState = (training) => normalizeTrainingStateLogic(training, { uid });
|
||||
const syncTrainingLedger = () => {
|
||||
@@ -1563,6 +1563,8 @@ const TRANSLATIONS = {
|
||||
"timing.stop": "Stoppa",
|
||||
"timing.reset": "Nollställ data",
|
||||
"timing.lap_remove": "Ta bort",
|
||||
"timing.corrections_in_judging": "Straff och varvändringar görs av domaren i Domarvyn (loggas och kan ångras).",
|
||||
"timing.open_in_judging": "Öppna i Domarvyn",
|
||||
"timing.lap_restore": "Återställ",
|
||||
"timing.lap_removed": "borttaget",
|
||||
"suspect.title": "Misstänkt korta varv ({count})",
|
||||
@@ -2035,9 +2037,9 @@ const TRANSLATIONS = {
|
||||
"guide.validation_2": "Ogiltiga kortvarv under min-gränsen räknas inte alls i leaderboard eller statistik.",
|
||||
"guide.validation_3": "Ogiltiga långvarv över max-gränsen räknas inte som varv, men de kan bryta lap-basen så nästa giltiga varv börjar om korrekt.",
|
||||
"guide.validation_4": "När ordinarie tid är slut kan sessionen gå in i Follow-up aktiv om du har satt Follow-up tid i raceformat eller sessionen.",
|
||||
"guide.validation_5": "I Tidtagning -> Detaljer kan du ge +1/-1 varv och +1/+5/-1 sekunder som manuell korrigering. Det slår igenom direkt i leaderboarden.",
|
||||
"guide.validation_6": "I samma detaljvy kan du också manuellt ogiltigförklara senaste räknade varvet om du behöver ta bort en felträff i efterhand.",
|
||||
"guide.validation_7": "Menyn Domare samlar samma korrigeringar i en separat arbetsvy med leaderboard, lap history och domarlogg för pågående session.",
|
||||
"guide.validation_5": "Straff (+1/-1 varv, +1/+5/-1 sekunder) finns bara i race (kval, heat, final) och görs av domaren i Domarvyn. Det slår igenom direkt i leaderboarden. Tidtagning -> Detaljer visar korrigeringarna och har knappen Öppna i Domarvyn.",
|
||||
"guide.validation_6": "Under träning tar du bort, återställer eller godkänner valfritt varv direkt i Tidtagning -> Detaljer. Misstänkt korta varv (genvägar) flaggas med ⚠ och listas med Godkänn / Ta bort på Tidtagning och Race Day.",
|
||||
"guide.validation_7": "Domarvyn har leaderboard, varvhistorik med Ta bort / Återställ per varv, straff (endast race) och domarlogg med ångra för pågående session.",
|
||||
"guide.validation_8": "Domarvyn kan filtrera på ogiltiga rader, korrigerade rader eller teamrace, exportera domarloggen och ångra flera senaste manuella åtgärder via undo-knappar.",
|
||||
"guide.qualifying_title": "Seedning, poängtabeller och tie-break",
|
||||
"guide.qualifying_1": "Practice och kval kan nu använda tre seedmetoder: bästa N varv som summa, bästa N varv som snitt eller bästa N konsekutiva varv.",
|
||||
@@ -3225,6 +3227,8 @@ const TRANSLATIONS = {
|
||||
"timing.stop": "Stop",
|
||||
"timing.reset": "Reset Data",
|
||||
"timing.lap_remove": "Remove",
|
||||
"timing.corrections_in_judging": "Penalties and lap changes are made by the judge in the Judging view (logged and undoable).",
|
||||
"timing.open_in_judging": "Open in Judging",
|
||||
"timing.lap_restore": "Restore",
|
||||
"timing.lap_removed": "removed",
|
||||
"suspect.title": "Suspiciously short laps ({count})",
|
||||
@@ -3697,9 +3701,9 @@ const TRANSLATIONS = {
|
||||
"guide.validation_2": "Invalid short laps under the minimum threshold do not count in the leaderboard or statistics.",
|
||||
"guide.validation_3": "Invalid long laps over the maximum threshold do not count as laps, but they can reset the lap base so the next valid lap starts correctly.",
|
||||
"guide.validation_4": "When the scheduled time ends, the session can enter Follow-up active if Follow-up time has been configured in race format or on the session.",
|
||||
"guide.validation_5": "In Timing -> Details you can apply +1/-1 lap and +1/+5/-1 seconds as manual corrections. The leaderboard updates immediately.",
|
||||
"guide.validation_6": "In the same detail view you can also manually invalidate the latest counted lap if you need to remove a false hit afterwards.",
|
||||
"guide.validation_7": "The Judging menu collects the same corrections in a separate work view with leaderboard, lap history and a judging log for the current session.",
|
||||
"guide.validation_5": "Penalties (+1/-1 lap, +1/+5/-1 seconds) exist only in races (qualifying, heats, finals) and are given by the judge in the Judging view. The leaderboard updates immediately. Timing -> Details shows the corrections and has an Open in Judging button.",
|
||||
"guide.validation_6": "In practice, remove, restore or approve any lap right in Timing -> Details. Suspiciously short laps (shortcuts) are flagged with ⚠ and listed with Approve / Remove on Timing and Race Day.",
|
||||
"guide.validation_7": "The Judging view has the leaderboard, lap history with Remove / Restore per lap, penalties (races only) and the judging log with undo for the current session.",
|
||||
"guide.validation_8": "The Judging view can filter invalid rows, corrected rows or team race rows, export the judging log and undo multiple recent manual actions via undo buttons.",
|
||||
"guide.qualifying_title": "Seeding, points tables and tie-break",
|
||||
"guide.qualifying_1": "Practice and qualifying can now use three seed methods: best N laps as total, best N laps as average or best N consecutive laps.",
|
||||
|
||||
@@ -227,6 +227,10 @@ function renderQuickAddPanel(session, deps) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Penalties belong to races only, and in races every correction is made by the judge in
|
||||
// the Judging view (logged, undoable). Practice keeps quick lap clean-up on Timing.
|
||||
const isPracticeType = (type) => ["open_practice", "free_practice", "practice"].includes(String(type || ""));
|
||||
|
||||
function renderLapActions(passing, rowKey, { t, escapeHtml }) {
|
||||
const button = (action, label, extra = "") => `<button class="btn btn-mini ${extra}" type="button" data-lap-action="${action}" data-lap-row="${escapeHtml(rowKey)}" data-lap-ts="${Number(passing.timestamp || 0)}">${escapeHtml(t(label))}</button>`;
|
||||
if (passing.validLap === false) return passing.invalidReason === "manual_invalid" ? button("restore", "timing.lap_restore") : "";
|
||||
@@ -276,21 +280,14 @@ function renderLeaderboardModal(session, row, deps) {
|
||||
<p>${t("table.pit")}: ${row.pitActive ? t("timing.pit_active") : `${Number(row.pitStops || 0)} / ${formatLap(row.totalPitMs)}`}</p>
|
||||
<p>${t("table.own_delta")}: ${escapeHtml(row.lapDelta || "-")}</p>
|
||||
</div>
|
||||
${isPracticeType(session.type) ? "" : `
|
||||
<div class="panel-body">
|
||||
<h4>${t("timing.manual_corrections")}</h4>
|
||||
<p>${t("timing.lap_adjustment")}: ${Number(row.manualLapAdjustment || 0) || 0}</p>
|
||||
<p>${t("timing.time_penalty")}: ${(Number(row.manualTimeAdjustmentMs || 0) || 0) > 0 ? "+" : (Number(row.manualTimeAdjustmentMs || 0) || 0) < 0 ? "-" : ""}${formatLap(Math.abs(Number(row.manualTimeAdjustmentMs || 0) || 0))}</p>
|
||||
<div class="actions-inline">
|
||||
<button class="btn btn-mini" id="corrLapPlus" type="button">${t("timing.penalty_add_lap")}</button>
|
||||
<button class="btn btn-mini" id="corrLapMinus" type="button">${t("timing.penalty_remove_lap")}</button>
|
||||
<button class="btn btn-mini" id="corrSecPlus" type="button">${t("timing.penalty_add_sec")}</button>
|
||||
<button class="btn btn-mini" id="corr5SecPlus" type="button">${t("timing.penalty_add_5sec")}</button>
|
||||
<button class="btn btn-mini" id="corrSecMinus" type="button">${t("timing.penalty_remove_sec")}</button>
|
||||
<button class="btn btn-mini" id="corrInvalidateLast" type="button">${t("timing.invalidate_last_lap")}</button>
|
||||
<button class="btn btn-mini" id="corrRestoreInvalid" type="button">${t("timing.restore_last_invalid")}</button>
|
||||
<button class="btn btn-danger btn-mini" id="corrReset" type="button">${t("timing.penalty_reset")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">${t("timing.corrections_in_judging")}</p>
|
||||
<button class="btn btn-mini" id="corrOpenJudging" type="button">${t("timing.open_in_judging")}</button>
|
||||
</div>`}
|
||||
<div class="panel-body">
|
||||
<h4>${t("timing.lap_history")}</h4>
|
||||
${
|
||||
@@ -306,7 +303,7 @@ function renderLeaderboardModal(session, row, deps) {
|
||||
<td>${counted ? lapNumber : "–"}</td>
|
||||
<td>${formatRaceClock(Math.max(0, passing.timestamp - (session.startedAt || passing.timestamp)))}</td>
|
||||
<td>${formatLap(passing.lapMs)}${passing.suspect && counted ? ` <span class="suspect-flag">⚠</span>` : ""}${counted ? "" : ` <small>${escapeHtml(t(passing.invalidReason === "manual_invalid" ? "timing.lap_removed" : "timing.invalid_short"))}</small>`}</td>
|
||||
<td class="lap-actions">${renderLapActions(passing, row.key, { t, escapeHtml })}</td>
|
||||
<td class="lap-actions">${isPracticeType(session.type) ? renderLapActions(passing, row.key, { t, escapeHtml }) : ""}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@@ -407,13 +404,13 @@ export function renderTimingView(deps) {
|
||||
state, dom, t, escapeHtml, formatLap, formatCountdown, formatElapsedClock, renderTable, uid,
|
||||
getActiveSession, ensureSessionResult, buildLeaderboard, getSessionTiming, getVisiblePassings,
|
||||
getEventName, getSessionTypeLabel, getStatusLabel, normalizeStartMode, getStartModeLabel, renderPositionGrid,
|
||||
connectDecoder, disconnectDecoder, applyCompetitorCorrection, invalidateCompetitorLastLap, restoreCompetitorLastInvalidLap,
|
||||
connectDecoder, disconnectDecoder,
|
||||
startOperationalSession, finishOperationalSession, cancelRaceStart, triggerRaceAudio, getStartLightState, saveState, updateHeaderState, renderView,
|
||||
normalizeDriver, normalizeCar, processDecoderMessage, getSelectedLeaderboardKey, setSelectedLeaderboardKey,
|
||||
getQuickAddDraft, setQuickAddDraft, getPassingValidationLabel, isCountedPassing, getCompetitorPassings,
|
||||
getManualCorrectionSummary, formatTeamActiveMemberLabel, formatRaceClock, renderSessionChannelAnalysis,
|
||||
setLastFinishAnnouncementSessionId, lastOverlayLeaderKeyBySession, lastOverlayTop3BySession, overlayEvents, setOverlayEvents, sessionReplay,
|
||||
syncTrainingLedger = () => {}, setCompetitorLapStatus = () => false,
|
||||
syncTrainingLedger = () => {}, setCompetitorLapStatus = () => false, openJudging = () => {},
|
||||
} = deps;
|
||||
|
||||
const helperDeps = {
|
||||
@@ -654,38 +651,7 @@ export function renderTimingView(deps) {
|
||||
|
||||
if (active && selectedRow) {
|
||||
bindQuickAddActions(active, selectedRow.transponder, "leaderboardModal", helperDeps);
|
||||
document.getElementById("corrLapPlus")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { lapDelta: 1 });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrLapMinus")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { lapDelta: -1 });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrSecPlus")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 1000 });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corr5SecPlus")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { timeMsDelta: 5000 });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrSecMinus")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { timeMsDelta: -1000 });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrInvalidateLast")?.addEventListener("click", () => {
|
||||
invalidateCompetitorLastLap(active, selectedRow);
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrRestoreInvalid")?.addEventListener("click", () => {
|
||||
restoreCompetitorLastInvalidLap(active, selectedRow);
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrReset")?.addEventListener("click", () => {
|
||||
applyCompetitorCorrection(active, selectedRow, { reset: true });
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("corrOpenJudging")?.addEventListener("click", () => openJudging(selectedRow.key));
|
||||
}
|
||||
|
||||
if (active) {
|
||||
@@ -970,14 +936,15 @@ export function renderJudgingView(deps) {
|
||||
<p>${t("table.best_lap")}: ${formatLap(selectedRow.bestLapMs)}</p>
|
||||
<p>${t("table.last_lap")}: ${formatLap(selectedRow.lastLapMs)}</p>
|
||||
<div class="actions">
|
||||
${isPracticeType(active.type) ? "" : `
|
||||
<button class="btn" id="judgeLapPlus">${t("timing.penalty_add_lap")}</button>
|
||||
<button class="btn" id="judgeLapMinus">${t("timing.penalty_remove_lap")}</button>
|
||||
<button class="btn" id="judgeSecPlus">${t("timing.penalty_add_sec")}</button>
|
||||
<button class="btn" id="judgeFivePlus">${t("timing.penalty_add_5sec")}</button>
|
||||
<button class="btn" id="judgeSecMinus">${t("timing.penalty_remove_sec")}</button>
|
||||
<button class="btn" id="judgeSecMinus">${t("timing.penalty_remove_sec")}</button>`}
|
||||
<button class="btn" id="judgeInvalidate">${t("timing.invalidate_last_lap")}</button>
|
||||
<button class="btn" id="judgeRestore">${t("timing.restore_last_invalid")}</button>
|
||||
<button class="btn btn-danger" id="judgeReset">${t("timing.penalty_reset")}</button>
|
||||
${isPracticeType(active.type) ? "" : `<button class="btn btn-danger" id="judgeReset">${t("timing.penalty_reset")}</button>`}
|
||||
</div>
|
||||
<div class="mt-16">
|
||||
<h4>${t("timing.lap_history")}</h4>
|
||||
|
||||
Reference in New Issue
Block a user