Add local and public QR sharing for training profiles

This commit is contained in:
larssand
2026-09-03 20:15:13 +02:00
parent 4f847610dc
commit 2d56b74731
15 changed files with 965 additions and 226 deletions

View File

@@ -69,6 +69,7 @@ test("backend hydration normalizes persisted entities and settings", () => {
pitEntryWsUrl: "ws://decoder:9002",
pitExitWsUrl: "ws://decoder:9003",
backendUrl: "http://race:8081",
publicBaseUrl: "https://training.example.test",
language: "en",
theme: "light",
autoReconnect: false,
@@ -107,6 +108,7 @@ test("backend hydration normalizes persisted entities and settings", () => {
assert.equal(state.activeSessionId, "session_1");
assert.deepEqual(state.audioEventLog, [{ id: "audio_1", event: "race_start" }]);
assert.equal(state.settings.theme, "light");
assert.equal(state.settings.publicBaseUrl, "https://training.example.test");
assert.equal(state.settings.splitDecoderEnabled, true);
assert.equal(state.settings.splitWsUrl, "ws://decoder:9001");
assert.equal(state.settings.pitDecoderEnabled, true);

View File

@@ -6,6 +6,7 @@ import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import QRCode from "qrcode";
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -147,8 +148,14 @@ test("SQLite API persists state, passings, and overlay session results", { timeo
const initialState = {
events: [{ id: "event_1", name: "Club race" }],
classes: [{ id: "class_1", name: "Buggy" }],
drivers: [{ id: "driver_1", name: "Driver One", classId: "class_1" }],
sessions: [{ id: "session_1", eventId: "event_1", name: "A final" }],
resultsBySession: {},
settings: { publicBaseUrl: "https://racecore.example.org" },
training: {
shares: [{ driverId: "driver_1", token: "test-share-token-1234567890", enabled: true }],
},
};
const saveResponse = await fetch(`${baseUrl}/api/state`, {
method: "POST",
@@ -160,6 +167,28 @@ test("SQLite API persists state, passings, and overlay session results", { timeo
assert.equal(savePayload.ok, true);
assert.equal(typeof savePayload.updatedAt, "string");
const shareToken = initialState.training.shares[0].token;
const publicQrResponse = await fetch(
`${baseUrl}/public-training/qr/${shareToken}?lang=en&baseUrl=${encodeURIComponent("https://public.example.test")}`,
);
const publicQrSvg = await publicQrResponse.text();
const expectedPublicQrSvg = await QRCode.toString(
`https://public.example.test/public-training/${shareToken}?lang=en`,
{ type: "svg", errorCorrectionLevel: "M", margin: 2, width: 320 },
);
assert.equal(publicQrResponse.status, 200);
assert.match(publicQrResponse.headers.get("content-type"), /^image\/svg\+xml/);
assert.equal(publicQrSvg, expectedPublicQrSvg);
const localQrResponse = await fetch(`${baseUrl}/public-training/qr/${shareToken}?lang=sv&baseUrl=local`);
const localQrSvg = await localQrResponse.text();
assert.equal(localQrResponse.status, 200);
assert.match(localQrResponse.headers.get("content-type"), /^image\/svg\+xml/);
assert.notEqual(localQrSvg, publicQrSvg);
const missingQrResponse = await fetch(`${baseUrl}/public-training/qr/unknown-share-token-123456?baseUrl=local`);
assert.equal(missingQrResponse.status, 404);
const backupResponse = await fetch(`${baseUrl}/api/backups`, {
method: "POST",
headers: { "Content-Type": "application/json" },