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)
- Production URLs unchanged.
- iOS verified on both iPad (
B193DBAA-3334-5381-B514-5E18BFC11682) and iPhone 17 (7EE28F79-506A-546A-BBC5-93B146E47E08). Both stream to the production ws-server using their auto-generated<color>-<bird>device names.
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.fillicon + "Ready" + "Certificate installed and trusted" when all three steps complete. - Yellow +
exclamationmark.shield.fillicon + "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.
- Green +
- 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 — c → co → cop → copp → coppe → … copper-pheasant were all distinct entries. Verified by the user with a screenshot of the sidebar.
Fix:
- Form fields no longer auto-save. Local
@Statecarries 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
cancellationActionslot whenever there are unsaved edits — reverts the draft to disk values. - Save is atomic: one call to
AppSettings.save+ onestreamer.applyafter 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
deviceStateentry. - 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-removedvariant added toBroadcastMessage.
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-removedbroadcasts 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 installcycle. - 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 athttps://busymate-helper-server.serebano.deno.net. - Dashboard sidebar shows live
Online/Offline+Connected/Disconnectedpills for both, ⋯ menu has Info / Settings / Delete device.
Where to start next session
- Auth — still none. Anyone with the production URLs can connect a fake device or read the dashboard. Pre-shared token on
/ingestand/dashboardis the natural next chunk if this ever leaves a personal subnet. - 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.
- 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.