buildSimplePdf's font declared no /Encoding at all, so PDF viewers fell
back to a standard font's built-in encoding, which has no å/ä/ö glyphs
- and separately, the content stream was written as UTF-8 instead of a
single-byte encoding, so even a correctly-encoded font would receive
the wrong bytes (a multi-byte UTF-8 sequence where PDF expects exactly
one byte per character): the classic "åäö" mojibake pattern.
Fixed by declaring /Encoding /WinAnsiEncoding and writing the content
stream through a proper WinAnsi (~CP1252) byte mapper instead of UTF-8.
This is the single shared renderer behind every PDF export (race
results, start lists, heat sheets, team reports, the results archive),
so the fix applies everywhere at once, matching what was reported.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A driver completing the practice timer, "Jämna varv", or "Stegträning"
now gets a permanent history entry instead of the outcome only existing
as a one-off notification - visible on both their mobile profile and
the admin Training page, so progress over past sessions is actually
reviewable. Each completion earns a prominent emoji-badged credit.
The completion is verified server-side against the driver's real
recorded laps before being saved, reusing the same progress builders
live tracking already uses - the client can only ask for a completion
to be checked and recorded, never supply the outcome itself, so a
driver can't fabricate a completed session (and the reward tied to it)
any more than they could fabricate a lap time.
Both per-driver and club-wide goals can now target "complete N
sessions of one exercise kind" as an alternative to "N laps", sharing
the existing reward/claim mechanism and forms.
Fixes a real bug found while building this: a pre-existing two-argument
wrapper around getTrainingGoalProgress in app.js silently dropped the
new third argument, so every sessions-metric goal displayed 0 progress
regardless of how many sessions were actually completed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Until now there was no way to find a completed session without already
knowing which event it belonged to - navigation was strictly Events ->
one event -> its sessions. A new "Resultatarkiv" view lists every
finished session across all events, searchable by event/session name,
class, driver and date range.
Opening a session reuses the existing single-session leaderboard
builder for standings and the existing per-competitor lap lookup for
lap history - no new results logic - plus a session-wide penalties/
corrections list (resultsBySession[id].adjustments), previously only
visible in the live judging action log. Export buttons reuse the
existing per-event PDF and race-package JSON export functions rather
than adding a new format.
Investigated what should happen to raw timing data when a race is
deleted. Training statistics and session replay already read from
their own independent, deliberately-retained copies that survive
deletion by existing design - unaffected either way. The one thing
that outlived deletion with no purpose was the raw `passings` SQLite
rows (write-only; nothing in the app reads them), growing forever with
driver IDs never purged. A new POST /api/passings/purge endpoint now
removes exactly the deleted session(s)' rows, called as a best-effort
side call from both delete-event and delete-session.
Also fixes a real bug found while building this: ensureSessionResult
guarded adjustments/splits/pitEvents/pitStops against being missing on
an already-existing result, but not passings itself, so viewing lap
history for such a result (e.g. an older/imported race package)
crashed outright.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigated whether two admin browsers open at once can overwrite each
other's changes, per an explicit request to report the risk before
making bigger changes. Found that POST /api/state always replaces the
entire stored document with a client's in-memory copy, with no version
check and no per-field merge - and a browser only ever pushes, never
re-fetching server state after page load except on the Overlay view.
An idle browser's next save, even an unrelated edit, could silently
revert another browser's more recent changes, worst case a live race's
in-progress results.
Two scoped fixes instead of the bigger optimistic-locking rework (that
needs real UX decisions about conflicting saves, deferred separately):
1. server.js now remembers which session results were just written by
a real live decoder passing (/api/passings) and protects that key
for 15 seconds against being reverted by a stale full-state save,
while still applying the rest of that save normally.
2. Every non-Overlay view now quietly refreshes its data in the
background every 15 seconds (skipped while a local edit is
pending), without ever calling renderView(), so it can never
discard an open, unsaved edit form - shrinking how stale a
browser's next save can be from hours to seconds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"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>