Career v2, WS2. Nothing measured play time before (the achievements
plugin's final-position shortcut double-counts loops and mis-reads
seeks). Now:
- stats-recorder.js accrues WALL-CLOCK seconds across song:play/resume ↔
pause/stop/ended spans (single spans clamp at 2h against suspend
inflation) and piggybacks them as `seconds` on the POSTs it already
sends; failed POSTs restore the accumulator; a session reset flushes
first so time can't re-attribute to the next song/arrangement.
- POST /api/stats accepts optional `seconds` (finite, 0 < s ≤ 6h) on the
scored and position branches, plus a new seconds-only branch for
unscored plays that ran to the natural end — banks time WITHOUT
touching the resume position (song:ended must not overwrite Continue)
and still counts as playing today for the streak.
- song_stats gains additive idempotent `seconds_total`; record_session/
touch_position accrue, new add_play_seconds() for the seconds-only
path; the legacy-encoding stats merge sums seconds across duplicates.
- Passports surface it: "14.2 h in Blues" under the badge stamp and on
the shelf cover sub-line — a true fact that only grows, never a
target or a meter (Stage 5 post-cap, per the career design).
Tests: seconds accrual/validation/seconds-only branch (stats API),
per-instrument-and-genre summing (career), fmtHours formatting (vm).
Full suites: pytest 2480, JS 1165.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>