From a8a81b04298392d2822d7e88b673f7fa01fde17a Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Wed, 22 Jul 2026 15:10:59 +0000 Subject: [PATCH] docs: update documentation to OKF v0.1 format --- docs/guides/admin-general-settings.md | 63 +++++++++++++++++++++++++-- docs/index.md | 2 +- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/guides/admin-general-settings.md b/docs/guides/admin-general-settings.md index df3f3b3..d617c83 100644 --- a/docs/guides/admin-general-settings.md +++ b/docs/guides/admin-general-settings.md @@ -2,8 +2,8 @@ type: guide title: Admin — General Settings description: How an admin edits required platform-wide settings, including the validated event window, from the /admin/general page. -tags: [guide, admin, settings, general, tester, datetime, validation] -timestamp: 2026-07-22T14:50:25Z +tags: [guide, admin, settings, general, tester, datetime, validation, utc, timezone] +timestamp: 2026-07-22T15:09:39Z --- # When this view is available @@ -56,7 +56,8 @@ The page is a single reactive form with these controls (every * **Initial load:** `loading() === true` renders `general-loading`. After both requests resolve, the form is patched with the values from the backend (UTC timestamps are converted to `datetime-local` strings via - `toDatetimeLocal` so the native picker shows them). + `toDatetimeLocal` so the native picker shows them — see + [Datetime UTC round-trip](#datetime-utc-round-trip) below). * **Logo upload:** selecting a file fires `POST /api/v1/uploads/logo`, then writes the returned `publicUrl` into the hidden `logo` control. If the upload fails, `general-logo-error` shows the message; the @@ -179,6 +180,62 @@ is rendered inside the per-field error region (`data-testid="general-eventEnd-error"`) by the `eventEndFieldMessage` helper. +# Datetime UTC round-trip + +Both Event-window fields (`general-eventStart`, `general-eventEnd`) are +backed by HTML `` controls, which the +browser renders as a wall-clock picker **without** any timezone +indicator. The two pure helpers exported from +`frontend/src/app/features/admin/general.pure.ts` make the wall-clock +values behave as if they were already UTC, so the admin sees the same +instant no matter where the browser is running: + +| Helper | Direction | Purpose | +|------------------------------------------------|-------------------------------------|---------| +| `toDatetimeLocal(iso: string): string` | ISO-8601 UTC → `YYYY-MM-DDTHH:mm` | Renders a backend UTC instant in the native picker using `getUTC*` getters. | +| `toIsoUtc(local: string): string` | `YYYY-MM-DDTHH:mm[:ss[.fff]]` → ISO-8601 UTC | Parses the wall-clock components as **UTC** (not local time) via `Date.UTC(...)` and returns `toISOString()`. | + +The `toIsoUtc` helper explicitly does **not** call `new Date(local)`, +which would interpret the components in the browser's local timezone +and shift the round-tripped instant by the browser's UTC offset. Instead +it regex-parses the year / month / day / hour / minute components (and +optional seconds / milliseconds), constructs the instant with +`new Date(Date.UTC(...))`, and returns `toISOString()`. As a result, +`toIsoUtc(toDatetimeLocal(iso)) === iso` for any minute-precision +`datetime-local` value the picker can produce, regardless of the +browser's `Intl.DateTimeFormat().resolvedOptions().timeZone` value. + +The helper preserves its existing fallback contract: + +| Input | Output | +|------------------------------------|---------------------------------| +| `''` (empty) | `''` | +| Non-matching `YYYY-MM-DDTHH:mm…` | Returns the input verbatim so the reactive-form `invalidDatetime` validator still fires. | +| Matching `YYYY-MM-DDTHH:mm` | `:00.000Z` | +| Matching `YYYY-MM-DDTHH:mm:ss` | `.000Z` | +| Matching `YYYY-MM-DDTHH:mm:ss.fff` | `Z` (fraction padded to ms) | + +`AdminGeneralComponent` calls `toIsoUtc` on both `eventStartUtc` and +`eventEndUtc` before issuing `PUT /api/v1/admin/general/settings`, so +correcting the shared helper fixes start and end submissions without +any further component, form, or backend rewiring. The backend zod +schema (`GeneralSettingsSchema` in +`backend/src/modules/admin/dto/general.dto.ts`) accepts the resulting +timezone-aware ISO-8601 strings unchanged and enforces +`eventEndUtc > eventStartUtc` via `superRefine`. + +The regression for this fix lives in +`tests/frontend/admin-general-pure.spec.ts` under the `datetime helpers` +suite, with cases that lock in: + +* `toIsoUtc('2027-03-15T08:30')` → `'2027-03-15T08:30:00.000Z'` + (would previously have returned `'2027-03-15T15:30:00.000Z'` from a + PDT browser). +* `toIsoUtc('2027-03-15T08:30:15')` → `'2027-03-15T08:30:15.000Z'`. +* `toIsoUtc('2027-03-15T08:30:15.123')` → `'2027-03-15T08:30:15.123Z'`. +* `toIsoUtc(toDatetimeLocal('2027-03-15T08:30:00.000Z'))` round-trips + back to the original ISO string. + # Visual elements | Element | Selector | diff --git a/docs/index.md b/docs/index.md index ac6c30f..b0bfccd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin controls. The docs below are organized by purpose so agents can pull just the slice -they need. Last regenerated 2026-07-22T14:50:25Z. +they need. Last regenerated 2026-07-22T15:09:39Z. # Architecture