Times were always showing 0 because ISO8601DateFormatter with default
options does not parse fractional seconds, but Prisma/Node.js serialises
dates as "2026-02-20T09:00:00.000Z" (with .000). Every date(from:) call
silently returned nil, so elapsedTime and duration always fell back to 0.
- Date+Extensions: fromISO8601 now tries .withFractionalSeconds first,
then falls back to whole seconds — single place to maintain
- OngoingTimer.elapsedTime: use Date.fromISO8601() instead of bare formatter
- TimeEntry.duration: use Date.fromISO8601() instead of bare formatters
- TimerView: add TimerUnitLabels view showing h/min/sec column headers
under the monospaced clock digits
GET /clients and GET /projects return bare arrays, not wrapped objects.
Remove ClientListResponse and ProjectListResponse wrapper structs and
update ClientsViewModel, ProjectsViewModel, and TimerViewModel to decode
[Client] and [Project] directly.
Keychain writes silently failed (missing keychain-access-groups entitlement
on simulator), causing the token to disappear between handleTokenResponse
and the first API call. The in-memory cache ensures the token is always
available within the session; the keychain still persists it across launches
when entitlements allow.
iOS clients now exchange the OIDC authorization code for a backend-signed
HS256 JWT via POST /auth/token. All subsequent API requests authenticate
using this JWT as a Bearer token, verified locally — no per-request IDP
call is needed. Web frontend session-cookie auth is unchanged.