fix status and roadmap
This commit is contained in:
80
src/app.js
80
src/app.js
@@ -1,3 +1,4 @@
|
||||
import { getSyncStatus } from "./sync_status.js";
|
||||
import { installListSorting } from "./list_sorting.js";
|
||||
import { escapeHtml } from "./html_escape.js";
|
||||
import { getOverlayModeLabel, buildOverlayPanels, renderOverlaySidePanel, renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } from "./overlays.js";
|
||||
@@ -142,7 +143,12 @@ const hydrateFromBackend = () => hydrateFromBackendHelper({ backend, t, getBacke
|
||||
const scheduleBackendSync = () => scheduleBackendSyncHelper({ getBackendSyncTimer: () => backendSyncTimer, setBackendSyncTimer: (value) => { backendSyncTimer = value; }, syncStateToBackend });
|
||||
const syncStateToBackend = () => syncStateToBackendHelper({ backend, t, getBackendUrl, buildPersistableState, getLocalStateVersion: () => localStateVersion, getSyncedStateVersion: () => syncedStateVersion, setSyncedStateVersion: (value) => { syncedStateVersion = value; }, getLocalStateDirty: () => localStateDirty, setLocalStateDirty: (value) => { localStateDirty = value; } });
|
||||
const pingBackend = () => pingBackendHelper({ backend, t, getBackendUrl });
|
||||
const checkAppVersion = () => checkAppVersionHelper({ onVersion: (version) => {
|
||||
const checkAppVersion = () => checkAppVersionHelper({ onContact: (online) => {
|
||||
backend.available = online;
|
||||
backend.connectionChecked = true;
|
||||
if (online) backend.lastContactAt = Date.now();
|
||||
updateSyncStatus();
|
||||
}, onVersion: (version) => {
|
||||
const label = document.getElementById("appVersionLabel");
|
||||
if (label) { label.textContent = `RC RaceCore v${version}`; label.hidden = false; }
|
||||
}, getBackendUrl, getBaselineAppVersion: () => baselineAppVersion, setBaselineAppVersion: (value) => { baselineAppVersion = value; }, getPendingAppVersion: () => pendingAppVersion, setPendingAppVersion: (value) => { pendingAppVersion = value; }, getLocalStateDirty: () => localStateDirty });
|
||||
@@ -379,6 +385,18 @@ const TRANSLATIONS = {
|
||||
"race_day.finish": "Avsluta heat",
|
||||
"race_day.start_list": "Startlista",
|
||||
"race_day.no_start_list": "Ingen startlista finns för sessionen.",
|
||||
"sync.connecting": "Ansluter till servern",
|
||||
"sync.connected": "Server ansluten",
|
||||
"sync.saved": "Ändringar sparade",
|
||||
"sync.pending": "Ändringar väntar på sparning",
|
||||
"sync.saving": "Sparar till servern…",
|
||||
"sync.error": "Sparning misslyckades",
|
||||
"sync.offline": "Serveranslutning saknas",
|
||||
"sync.stale": "Serverstatus är inaktuell",
|
||||
"sync.retry": "Försök igen",
|
||||
"sync.loaded": "Data hämtad {time}",
|
||||
"sync.saved_at": "Sparat {time}",
|
||||
"sync.stale_hint": "Visad data kan vara inaktuell.",
|
||||
"race_day.preflight": "Förkontroll",
|
||||
"race_day.check_summary": "{blockers} blockerar start · {warnings} varningar",
|
||||
"race_day.heat_queue": "Heat-kö",
|
||||
@@ -1604,7 +1622,7 @@ const TRANSLATIONS = {
|
||||
"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 -- <fil.sqlite> --confirm.",
|
||||
"guide.race_day_6": "6. Använd Race Day för start, paus, fortsätt och avslut. Menyn visar server- och sparstatus separat från decodern. Vid sparfel: behåll fliken öppen och klicka Försök igen; väntande ändringar är inte bekräftat sparade på servern. Tidtagning, Domare och Overlay finns som direkta genvägar. Lokal restore kräver stoppad server och npm run backup:restore -- <fil.sqlite> --confirm.",
|
||||
"guide.race_day_7": "7. Session replay i Tidtagning spelar upp sparade passeringar i ett isolerat testläge. Välj session, ladda och kör i 1x, 2x, 5x eller 10x utan att ändra originalresultat eller träningsstatistik.",
|
||||
"guide.dashboard_title": "Schemaavvikelse på Översikt",
|
||||
"guide.dashboard_1": "Översikt visar nu skillnaden mellan planerad tid och faktisk körtid för alla startade sessioner.",
|
||||
@@ -1753,6 +1771,18 @@ const TRANSLATIONS = {
|
||||
"race_day.finish": "Finish heat",
|
||||
"race_day.start_list": "Start list",
|
||||
"race_day.no_start_list": "No start list is available for this session.",
|
||||
"sync.connecting": "Connecting to server",
|
||||
"sync.connected": "Server connected",
|
||||
"sync.saved": "Changes saved",
|
||||
"sync.pending": "Changes waiting to save",
|
||||
"sync.saving": "Saving to server…",
|
||||
"sync.error": "Save failed",
|
||||
"sync.offline": "Server disconnected",
|
||||
"sync.stale": "Server status is stale",
|
||||
"sync.retry": "Retry",
|
||||
"sync.loaded": "Data loaded {time}",
|
||||
"sync.saved_at": "Saved {time}",
|
||||
"sync.stale_hint": "Displayed data may be out of date.",
|
||||
"race_day.preflight": "Preflight",
|
||||
"race_day.check_summary": "{blockers} blocking start · {warnings} warnings",
|
||||
"race_day.heat_queue": "Heat queue",
|
||||
@@ -2978,7 +3008,7 @@ const TRANSLATIONS = {
|
||||
"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 -- <file.sqlite> --confirm.",
|
||||
"guide.race_day_6": "6. Use Race Day for start, pause, resume, and finish. The menu shows server/save status separately from the decoder. On save failure, keep the tab open and click Retry; pending changes are not confirmed saved on the server. Timing, Judging, and Overlay remain one click away. Local restore requires a stopped server and npm run backup:restore -- <file.sqlite> --confirm.",
|
||||
"guide.race_day_7": "7. Session replay in Timing replays saved passings in an isolated test mode. Select a session, load it, and run at 1x, 2x, 5x, or 10x without modifying source results or training statistics.",
|
||||
"guide.dashboard_title": "Schedule drift on overview",
|
||||
"guide.dashboard_1": "Overview now shows the difference between planned time and actual elapsed time for all started sessions.",
|
||||
@@ -3398,6 +3428,17 @@ async function init() {
|
||||
updateConnectionBadge();
|
||||
tickClock();
|
||||
setInterval(tickClock, 1000);
|
||||
updateSyncStatus();
|
||||
setInterval(updateSyncStatus, 1000);
|
||||
document.getElementById("syncStatusRetry")?.addEventListener("click", async () => {
|
||||
if (backend.pendingWrites) return;
|
||||
if (localStateDirty || backend.writeError || backend.passingError) {
|
||||
await syncStateToBackend();
|
||||
if (!backend.writeError && !localStateDirty) backend.passingError = "";
|
||||
}
|
||||
await checkAppVersion();
|
||||
updateSyncStatus();
|
||||
});
|
||||
startAppVersionPolling();
|
||||
|
||||
Promise.allSettled([hydrateFromBackend(), loadAmmcConfigFromBackend(), refreshServerBackups({ render: false })]).finally(() => {
|
||||
@@ -3421,6 +3462,32 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSyncStatus() {
|
||||
const container = document.getElementById("syncStatus");
|
||||
if (!container) return;
|
||||
const status = getSyncStatus(backend, localStateDirty);
|
||||
container.dataset.state = status.kind;
|
||||
const label = document.getElementById("syncStatusLabel");
|
||||
const text = t("sync." + status.kind);
|
||||
if (label.textContent !== text) label.textContent = text;
|
||||
const detail = document.getElementById("syncStatusDetail");
|
||||
const parts = [];
|
||||
if (backend.lastLoadedAt) parts.push(t("sync.loaded", { time: new Date(backend.lastLoadedAt).toLocaleTimeString() }));
|
||||
if (backend.lastSavedAt) parts.push(t("sync.saved_at", { time: new Date(backend.lastSavedAt).toLocaleTimeString() }));
|
||||
if (!backend.available || status.kind === "stale") parts.push(t("sync.stale_hint"));
|
||||
detail.textContent = parts.join(" · ");
|
||||
const warning = document.getElementById("syncWarning");
|
||||
if (warning) {
|
||||
warning.hidden = !["offline", "stale", "error"].includes(status.kind);
|
||||
warning.textContent = `${text}. ${t("sync.stale_hint")}`;
|
||||
}
|
||||
container.title = backend.writeError || backend.passingError || backend.lastError || "";
|
||||
const retry = document.getElementById("syncStatusRetry");
|
||||
retry.textContent = t("sync.retry");
|
||||
retry.hidden = !status.canRetry;
|
||||
retry.disabled = Boolean(backend.pendingWrites);
|
||||
}
|
||||
|
||||
function markSettingsDraftActive(event) {
|
||||
if (currentView !== "settings") return;
|
||||
const target = event.target;
|
||||
@@ -3682,6 +3749,7 @@ function saveState(options = {}) {
|
||||
localStateVersion += 1;
|
||||
localStateDirty = true;
|
||||
scheduleBackendSync();
|
||||
updateSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4210,7 +4278,7 @@ function importDataPayload(parsed, mode = "full") {
|
||||
|
||||
function getDefaultBackendUrl() {
|
||||
if (window.location.protocol.startsWith("http") && window.location.hostname) {
|
||||
return `${window.location.protocol}//${window.location.hostname}:8081`;
|
||||
return window.location.origin;
|
||||
}
|
||||
return "http://127.0.0.1:8081";
|
||||
}
|
||||
@@ -5362,6 +5430,7 @@ function findDuplicateSessionTransponders(session) {
|
||||
}
|
||||
|
||||
async function persistPassingToBackend(sessionId, passing) {
|
||||
backend.pendingWrites = (backend.pendingWrites || 0) + 1;
|
||||
try {
|
||||
const res = await fetch(`${getBackendUrl()}/api/passings`, {
|
||||
method: "POST",
|
||||
@@ -5376,5 +5445,8 @@ async function persistPassingToBackend(sessionId, passing) {
|
||||
} catch (error) {
|
||||
backend.available = false;
|
||||
backend.lastError = t("error.passing_save_failed", { msg: error instanceof Error ? error.message : String(error) });
|
||||
backend.passingError = backend.lastError;
|
||||
} finally {
|
||||
backend.pendingWrites = Math.max(0, (backend.pendingWrites || 1) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user