2026-05-04

End-of-day snapshot. Three threads landed today: (1) the iOS Setup view simplified to a centered power toggle + readiness card pinned at the bottom, (2) the Web Streaming editor switched from auto-save-per-keystroke to an explicit Apply confirmation button in the nav bar, and (3) the dashboard gained a per-device Delete affordance + a server-side cascade so orphan devices stop accumulating.

Status: working as expected (deployed live)

Setup view layout

Two changes that ripple through the home screen experience:

  • Three step cards moved to a sheet. The Download / Install / Trust DisclosureGroups that lived inline on the Setup view are now in a new SetupStepsView, presented as a sheet. The home view keeps them out of sight when everything is set up.
  • Readiness card replaces them inline. A single big card-button that summarizes the cert state:
    • Green + checkmark.shield.fill icon + "Ready" + "Certificate installed and trusted" when all three steps complete.
    • Yellow + exclamationmark.shield.fill icon + "Not Ready" + a one-line description of what's next ("Tap to generate and download the BusymateHelper certificate." / "Profile downloaded — install it in iOS Settings → General → VPN & Device Management." / "Almost there — enable full trust for BusymateHelper in iOS Settings → General → About → Certificate Trust Settings.").
    • Tap → opens the steps sheet, with the next undone step auto-expanded.
  • Power glyph centered + bumped to 140 pt. The view now uses VStack { Spacer; vpnHeader; Spacer; readinessCard } instead of a List, so the power button sits vertically centered with the readiness card pinned at the bottom — the home view reads as "the big primary action lives here, the setup-card is the secondary surface."

Step header badges (numbered yellow / green-checked) and the iOS-style row UX inside the sheet are unchanged from yesterday.

Web Streaming — Apply-on-confirm

The previous .onChange(of: deviceName) { save() } design saved every keystroke, which created a new device on the dashboard per character — ccocopcoppcoppe → … copper-pheasant were all distinct entries. Verified by the user with a screenshot of the sidebar.

Fix:

  • Form fields no longer auto-save. Local @State carries the draft.
  • Apply button moved to the navigation bar's trailing position as a confirmationAction. Disabled when the draft matches what's on disk.
  • Discard button appears in the leading cancellationAction slot whenever there are unsaved edits — reverts the draft to disk values.
  • Save is atomic: one call to AppSettings.save + one streamer.apply after Apply. Never per-keystroke.

The remote-fetch refresh path (onChange(of: remoteSettings.lastFetch)) still overwrites local state when the server pushes new settings — that's intentional, server-managed remote settings should win, and the user can re-edit + Apply.

Dashboard — Device delete + server cascade

The orphan devices that accumulated from yesterday's auto-save bug needed a way out beyond waiting for the 7-day TTL.

ws-server:

  • DELETE /api/devices/:name — wipes a device entirely:
    • Closes the live ingest socket if connected.
    • Drops in-memory deviceState entry.
    • Calls clearEntries (existing) for ["entries", deviceName, …].
    • Deletes ["devices", deviceName] (device-list registry).
    • Deletes ["settings", "device", deviceName] (per-device override).
    • Broadcasts a new {type:"device-removed",device} envelope to every dashboard.
  • New device-removed variant added to BroadcastMessage.

Dashboard:

  • Sidebar ⋯ menu gains a third item below Settings, separated by a divider, in red: Delete device. Confirm dialog before the DELETE call.
  • Handles device-removed broadcasts by dropping the device's entries, status, and info from local state, plus clearing the active device-filter if the removed device was selected.

This pairs with the Apply-on-confirm fix: the user can now both prevent new orphans and clean up the existing ones.

Configuration verified this session

  • iPad — installed build 3 from a fresh xcodebuild/devicectl install cycle.
  • iPhone 17 — installed the same build, paired and DDI loaded, app launched first try (dev cert was already trusted from a prior session — no re-trust prompt).
  • Both devices streaming to production with streaming.enabled = true, the auto-generated <color>-<bird> names, and pointing at https://busymate-helper-server.serebano.deno.net.
  • Dashboard sidebar shows live Online/Offline + Connected/Disconnected pills for both, ⋯ menu has Info / Settings / Delete device.

Where to start next session

  1. Auth — still none. Anyone with the production URLs can connect a fake device or read the dashboard. Pre-shared token on /ingest and /dashboard is the natural next chunk if this ever leaves a personal subnet.
  2. Body-size cap on the live broadcast. ws-server only truncates KV writes (64 KiB cap). The live WS forwards full bodies to dashboards. A multi-MB capture from a single device fans out to every connected dashboard. A configurable cap in the iOS streamer would gate this.
  3. iPad re-trust workflow. The iPhone 17 deploy worked first-try because dev cert trust persisted. If iOS ever invalidates it (rare, but happened on BMH1), the user has to manually re-trust via Settings → VPN & Device Management. Worth documenting in a runbook if this recurs.