Skip to content

Latest commit

 

History

History
41 lines (30 loc) · 9.09 KB

File metadata and controls

41 lines (30 loc) · 9.09 KB

Trimmed hosted first-run wizard (C4)

  • Status: done
  • Date: 2026-06-20
  • Specs touched: FIRST_RUN.md (Phase 2 per-profile step set; corrected the hosted-trim summary to include telemetry), CLAUDE.md (standard structured fields — added zone)

Part of the cloud-VM track in #196. This is C4 (#208), the web-ui half that consumes the gate built in C3a (#206): the first-run wizard, trimmed for the hosted profile, plus the three brain endpoints its non-account steps need. C3a's closing paragraph named this work ("the wizard drops the network / storage / enrollment steps and adds the bootstrap-secret field to the first-admin step") and locked the contract that C4 reuses C3a's bootstrap_secret body field rather than inventing its own one-time URL — honored here.

The wizard's step set is data-driven on purpose: this slice ships the four steps ENVIRONMENT.md # Provisioning keeps for a hosted box — admin (incl. recovery), time zone, telemetry, done — and the same shell serves the appliance. The appliance's network/storage steps (FIRST_RUN.md # Step 1) and the enrollment step (# Step 5) are spliced into the same list by a later change (call it B4) per profile, without touching the shell.

What was done

  • internal/protocol/host.go + internal/hostclient/ — new SetTimezoneRequest{Zone} and Client.SetTimezone(ctx, zone) over POST /v1/system/set-timezone. The brain holds no time-zone state; timedatectl on the host is the source of truth, so the brain endpoint is a validated pass-through.
  • internal/hostagent/ (the host seam)TimezoneSetter consumer-side interface + an optional Timezone field on Agent, and the POST /v1/system/set-timezone handler. When the setter is nil (the cmd/host-agent fake / dev loop) set-timezone is an accepted no-op 200 so the wizard works end-to-end without a host; when wired it delegates and maps a setter error to 500. Empty zone → 400.
  • internal/hostagent/timezone/ (new provider)Setter.SetTimezone shells out to timedatectl set-timezone <zone>, guarded by a zone-shape regex (area[/location...], safe chars) at the privileged boundary. Isolated in its own package so the shared internal/hostagent carries no timedatectl dependency; wired by cmd/host-agent-real in both build profiles (wiring_hosted.go + wiring_appliance.go) — an appliance and a hosted VM both run in a time zone.
  • internal/store/ (box_meta) — two new keys on C3a's box_meta KV table (no migration): BoxMetaTelemetryConsent ("true"/"false"; unset ⇒ off) and BoxMetaFirstRunComplete ("true" once the wizard's Done step runs).
  • internal/api/firstrun.go (new) — three admin-gated endpoints the wizard's non-account steps call, also reused by Settings later: POST /system/timezone (validates the IANA shape → 422, then pass-through to host → 502 on host failure), POST /system/telemetry (persists consent), POST /system/first-run-complete (writes the marker, idempotent). Helpers profileName() (unset ⇒ appliance) and boxMetaBool().
  • internal/api/auth.goGET /auth/state now returns {has_users, first_run_complete, profile} (was has_users only): the dashboard gates the wizard on first_run_complete, not has_users, so a half-finished wizard resumes instead of dropping the user onto the dashboard; profile selects the wizard's step set and whether the admin step shows the bootstrap-secret field. POST /setup gained an optional recovery flag (FIRST_RUN.md # Step 2a) — nil/true generate the code as before (back-compat for the headless M1c /setup); explicit false is the wizard's "off" path: the admin row is created with no recovery hash and the response carries an empty recovery_code, so the public recover flow can never be redeemed for that account.
  • web-ui/src/Setup.vue rewritten as a profile-aware wizard shell rendering one step at a time from a data-driven list; new step components under web-ui/src/setup/ (AdminStep, TimezoneStep, TelemetryStep, DoneStep). auth.ts exposes firstRunComplete + profile, a setup(u, p, {recovery, bootstrapSecret}), and setSystemTimezone / setTelemetryConsent / completeFirstRun. App.vue renders the wizard while !firstRunComplete, with a !firstRunComplete && hasUsers && !currentUser → Login branch so an interrupted wizard re-authenticates before resuming (the remaining steps are admin-gated). Shared wizard controls (.check, .warn, select, button.link, .details) added to style.css.
  • Codegenmake openapi + npm run gen:api regenerated: the deltas are the three new operations, recovery on the setup request, and the two new /auth/state fields.
  • Testsinternal/api/firstrun_test.go (timezone 200/trim/422/comma-422-regression/host-502/403/401, telemetry persist-on+persist-off/403/401, first-run-complete persist + reflected in /auth/state + idempotent + 403/401, profile in /auth/state for appliance + hosted, setup recovery:true→code, recovery:false→no-code + empty hash + recover() 401); a fake set-timezone route added to newHarness with a 500-sentinel for the host-502 path; internal/hostagent/settimezone_test.go (nil no-op, empty-zone 400, wired-setter applies, wired-setter error 500); internal/hostagent/timezone/timezone_test.go (regex accept/reject + rejection-before-exec). make check + make check-web green.

How it maps to the specs

  • Realizes FIRST_RUN.md # Phase 2 for the hosted profile and ENVIRONMENT.md # Provisioning ("Setup wizard, trimmed: … what survives is the first admin account (including the recovery code) and time zone. Telemetry consent stays as specced"). FIRST_RUN.md's one-line hosted summary (line 5) previously said "trimmed to admin account + time zone", omitting telemetry; corrected to match ENVIRONMENT.md, and the Phase 2 step headings were marked per-profile (which steps the appliance vs hosted vs both show).
  • Telemetry copy is taken verbatim from TELEMETRY.md # Locked (the checkbox sentence and the PostHog third-party disclosure inside "What does this collect?").
  • The admin step's recovery toggle follows FIRST_RUN.md # Step 2a (on by default; off surfaces the tradeoff copy; the code is shown once on a save-it screen with a Copy button — clipboard API on hosted HTTPS, a hidden-textarea execCommand fallback for the appliance's non-secure http://*.local context — and an "I have saved this recovery code" checkbox that gates Continue). The bootstrap-secret field follows ENVIRONMENT.md # Admin bootstrap and reuses C3a's bootstrap_secret.

Known gaps & deviations

  • Time-zone detection uses the browser's Intl zone, not IP geolocation. FIRST_RUN.md # Step 3 specs IP-geolocation auto-detect with a list fallback. We default the picker to Intl.DateTimeFormat().resolvedOptions().timeZone and always show the full Intl.supportedValuesOf('timeZone') list — no server-side geolocation dependency, and for a hosted box configured "from my own laptop" the operator's browser zone is the better signal. The list is always shown (overridable here, not only in Settings).
  • The admin step collects a Linux username, not a display name. FIRST_RUN.md # Identity & display names specs a display-name field slugified to a stable Linux user id (transliterate, reserved-slug list, --/xn-- guards). That slugification is deferred; the wizard collects the username directly, matching the brain's current POST /setup contract (the walking-skeleton hardcoded "admin"). When the slug mapping lands, the admin step swaps the field with no shell change.
  • timezone.Setter's timedatectl exec is not unit-tested. Only the zone-shape regex guard is (the testable branch). Running the real shell-out would mutate the host clock config and need root — exercised in the host-integration lane, the same convention as usermgr (//go:build usermgrtest, root, nspawn) and clockhealth (parse-logic unit tests, real chronyc in the lane). Brain-side handlers are 85–100% covered; the remaining gaps are defensive store-500 branches the real-store/no-mock harness can't easily reach.
  • No VM exercise. Like C3a, the hosted path isn't booted end-to-end here (QEMU outer loop blocked on this box by #189); the wizard is verified on the appliance dev loop (make dev) and by the Go/web gates.

What's next

  • B4 — appliance/full wizard steps. Splice the network + storage steps (FIRST_RUN.md # Step 1) and the secure-URL/enrollment step (# Step 5) into the data-driven step list, per profile, reusing this shell.
  • Display-name → slug. Replace the admin step's username field with the display-name input + brain-side slugification (FIRST_RUN.md # Identity & display names).
  • Settings reuse. The three endpoints are admin-callable any time; Settings → System → Time and Settings → Privacy can drive POST /system/timezone and /system/telemetry directly.
  • End-to-end hosted boot. Assert the trimmed wizard (bootstrap-secret field → C3a gate → time zone → telemetry → done → dashboard) in QEMU once the seed-delivery path and #189 land.