Expose guided training exercises on the driver's mobile profile link

"Jämna varv" and "Stegträning" previously only lived on the admin
Training page. A driver's own personal mobile profile link now offers
the same self-service configure/start/stop flow already used for the
5-minute practice timer there, with live progress computed server-side
from the driver's own share token so raw lap rows and driver IDs never
reach the client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-09 09:37:38 +02:00
parent 01e1165748
commit 2846b4c0fe
9 changed files with 573 additions and 6 deletions

View File

@@ -360,7 +360,11 @@ app.get("/public-training/api/:shareToken", (req, res) => {
const stored = readStoredAppState();
const sinceTimestamp = publicTrainingProfile.normalizePublicTrainingSince(req.query.since, req.query.startedAgoMs);
const windowMs = publicTrainingProfile.normalizePublicTrainingWindow(req.query.windowMs);
const profile = stored ? publicTrainingProfile.buildPublicTrainingProfile(stored.state, String(req.params.shareToken || ""), stored.updatedAt, sinceTimestamp, windowMs) : null;
// Separate "since" markers for the two guided exercises: a driver may run one, both, or
// neither independently of the general practice timer above, each started at its own time.
const evenLapsSince = publicTrainingProfile.normalizePublicTrainingSince(req.query.evenLapsSince, req.query.evenLapsStartedAgoMs);
const stepTrainingSince = publicTrainingProfile.normalizePublicTrainingSince(req.query.stepTrainingSince, req.query.stepTrainingStartedAgoMs);
const profile = stored ? publicTrainingProfile.buildPublicTrainingProfile(stored.state, String(req.params.shareToken || ""), stored.updatedAt, sinceTimestamp, windowMs, evenLapsSince, stepTrainingSince) : null;
if (!profile) {
res.status(404).json({ error: "Profile not found" });
return;
@@ -388,6 +392,38 @@ app.post("/public-training/api/:shareToken/markers", (req, res) => {
}
});
app.post("/public-training/api/:shareToken/even-laps", (req, res) => {
try {
const stored = readStoredAppState();
const entry = stored ? publicTrainingProfile.savePublicEvenLapsConfig(stored.state, String(req.params.shareToken || ""), req.body || {}) : null;
if (!entry) {
res.status(404).json({ error: "Profile, class, or configuration not found" });
return;
}
const nowIso = new Date().toISOString();
db.prepare("INSERT INTO app_state (id, state_json, updated_at) VALUES (1, ?, ?) ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json, updated_at = excluded.updated_at").run(JSON.stringify(stored.state), nowIso);
res.status(201).json({ ok: true });
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
app.post("/public-training/api/:shareToken/step-training", (req, res) => {
try {
const stored = readStoredAppState();
const entry = stored ? publicTrainingProfile.savePublicStepTrainingConfig(stored.state, String(req.params.shareToken || ""), req.body || {}) : null;
if (!entry) {
res.status(404).json({ error: "Profile, class, or configuration not found" });
return;
}
const nowIso = new Date().toISOString();
db.prepare("INSERT INTO app_state (id, state_json, updated_at) VALUES (1, ?, ?) ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json, updated_at = excluded.updated_at").run(JSON.stringify(stored.state), nowIso);
res.status(201).json({ ok: true });
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
}
});
app.get("/public-training/qr/:shareToken", async (req, res) => {
try {
const stored = readStoredAppState();