diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..019ebe9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +User-visible changes to RC RaceCore are recorded here, starting with the 1.1.0 baseline. Earlier development has not been reconstructed as individual releases. + +The application version is maintained in `package.json` and displayed in the administration menu. Add changes under **Unreleased** as they are made. When preparing a release, move those entries into a dated version section and update the version in both `package.json` and `package-lock.json`. + +## Unreleased + +No changes recorded yet. + +## 1.1.0 - 2026-09-05 + +### Added + +- Visible application version in the menu, read from the running server's package version. +- This changelog as the starting point for tracking future changes. + +### Baseline + +The existing feature set includes race and sponsor-event setup, team racing, AMMC timing, Race Day preflight, overlays and OBS views, Swedish and English race audio, training statistics, personal mobile profiles, import/export, and backups. See the README and in-app guide for configuration and usage. + +Recent improvements included in this baseline: + +- More compact mobile training profiles and consistent class-selector text sizing. +- Mobile refresh protection while entering data, selecting a class, or saving a car change. +- Separate unsaved car-change notes per class during page updates and class switching. +- Preservation of loaded mobile statistics during temporary refresh failures. +- Race Day preflight lists blockers first and provides shortcuts to the relevant configuration. diff --git a/README.md b/README.md index 466ba9c..b6ef9dd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # RC RaceCore +The running version is shown at the bottom of the menu and comes from `package.json`. Changes are recorded in [CHANGELOG.md](CHANGELOG.md). + English README. Swedish version: [`README.sv.md`](README.sv.md) RC RaceCore is an RC timing and race-control system with support for sponsor events with shared cars/transponders, AMMC WebSocket input, and local persistence through SQLite. diff --git a/README.sv.md b/README.sv.md index 2791cc1..e652c12 100644 --- a/README.sv.md +++ b/README.sv.md @@ -1,5 +1,7 @@ # RC RaceCore +Versionen som körs visas längst ned i menyn och hämtas från `package.json`. Ändringar dokumenteras i [CHANGELOG.md](CHANGELOG.md). + RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika heat/finaler), AMMC WebSocket och lokal SQLite-lagring på Windows. ## Arkitektur diff --git a/index.html b/index.html index 8630af8..2dc0dc0 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,7 @@
diff --git a/server.js b/server.js index 6105d20..40b6094 100644 --- a/server.js +++ b/server.js @@ -35,6 +35,7 @@ db.pragma("foreign_keys = ON"); initSchema(); const backupService = createBackupService({ db, dataDir: DB_DIR }); const appVersion = { + version: require("./package.json").version, revision: 1, updatedAt: new Date().toISOString(), }; diff --git a/src/app.js b/src/app.js index f6253bb..f3c7102 100644 --- a/src/app.js +++ b/src/app.js @@ -140,7 +140,10 @@ 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({ getBackendUrl, getBaselineAppVersion: () => baselineAppVersion, setBaselineAppVersion: (value) => { baselineAppVersion = value; }, getPendingAppVersion: () => pendingAppVersion, setPendingAppVersion: (value) => { pendingAppVersion = value; }, getLocalStateDirty: () => localStateDirty }); +const checkAppVersion = () => checkAppVersionHelper({ 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 }); const startAppVersionPolling = () => startAppVersionPollingHelper({ checkAppVersion, getAppVersionPollTimer: () => appVersionPollTimer, setAppVersionPollTimer: (value) => { appVersionPollTimer = value; } }); const startOverlaySync = () => startOverlaySyncHelper({ getOverlaySyncTimer: () => overlaySyncTimer, setOverlaySyncTimer: (value) => { overlaySyncTimer = value; }, getLocalStateDirty: () => localStateDirty, hydrateFromBackend, getCurrentView: () => currentView, renderView }); const startOverlayRotation = () => startOverlayRotationHelper({ getOverlayRotationTimer: () => overlayRotationTimer, setOverlayRotationTimer: (value) => { overlayRotationTimer = value; }, getOverlayRotationIndex: () => overlayRotationIndex, setOverlayRotationIndex: (value) => { overlayRotationIndex = value; }, getCurrentView: () => currentView, getOverlayViewMode: () => overlayViewMode, renderView }); diff --git a/src/runtime_services.js b/src/runtime_services.js index 6c17ede..9c59df9 100644 --- a/src/runtime_services.js +++ b/src/runtime_services.js @@ -200,11 +200,12 @@ export async function pingBackendHelper({ backend, t, getBackendUrl }) { } } -export async function checkAppVersionHelper({ getBackendUrl, getBaselineAppVersion, setBaselineAppVersion, getPendingAppVersion, setPendingAppVersion, getLocalStateDirty }) { +export async function checkAppVersionHelper({ getBackendUrl, getBaselineAppVersion, setBaselineAppVersion, getPendingAppVersion, setPendingAppVersion, getLocalStateDirty, onVersion = () => {} }) { try { const res = await fetch(`${getBackendUrl()}/api/app-version`, { cache: "no-store" }); if (!res.ok) return; const payload = await res.json(); + if (typeof payload.version === "string" && payload.version) onVersion(payload.version); const key = `${payload.revision}:${payload.updatedAt}`; if (!getBaselineAppVersion()) { setBaselineAppVersion(key); diff --git a/src/styles.css b/src/styles.css index 0cfe50b..473373d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -203,6 +203,15 @@ body { ); } +.app-version { + display: block; + margin-top: 8px; + font-size: 0.72rem; + color: var(--muted); +} + +.app-version[hidden] { display: none; } + .sidebar-footer { position: static; margin-top: 14px; diff --git a/tests/server_persistence.integration.test.js b/tests/server_persistence.integration.test.js index 8549e69..ecae331 100644 --- a/tests/server_persistence.integration.test.js +++ b/tests/server_persistence.integration.test.js @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; @@ -91,6 +91,13 @@ test("SQLite API persists state, passings, and overlay session results", { timeo assert.equal(health.ok, true); assert.equal(health.dbPath, join(dataDir, "rc_timing.sqlite")); + const versionResponse = await fetch(`${baseUrl}/api/app-version`); + assert.equal(versionResponse.status, 200); + const appVersion = await versionResponse.json(); + assert.equal(appVersion.version, JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8")).version); + assert.equal(typeof appVersion.revision, "number"); + assert.ok(appVersion.updatedAt); + const sameOriginResponse = await fetch(`${baseUrl}/api/health`, { headers: { Origin: baseUrl } }); assert.equal(sameOriginResponse.status, 200); assert.equal(sameOriginResponse.headers.get("access-control-allow-origin"), baseUrl);