2026-05-16

Builds 26 → 31. Heavy day. Audit-driven cleanup + four real user-facing bugs fixed end-to-end.

What changed today

Dashboard

  • Rebranding. BusymateHelperBusymate on every UI surface (DashboardClient, SettingsShell, <head> title, login card, VersionBadge tooltip). iOS app name + bundle id stay BusymateHelper / com.busymatehelper.app — only the dashboard's display string changed.
  • Mobile header now renders Busymate v1.0 (N) inline under the app name on mobile, matching the desktop topbar.
  • /api/version whitelisted in proxy.ts PUBLIC_EXACT so BuildWatcher can poll it across all session states (pre-login, expired session). Previous behavior 307'd to /login, breaking auto-reload after a deploy.
  • Phantom-device delete at Settings → per-device → Settings tab now actually works:
    • DeviceSettingsTab.removeDevice was wired to onRemoved() taking no args. The parent just closed the modal. State, IDB, and per-device Maps kept the device name forever, so deleting felt like "nothing happens" — particularly on phantom rows (cached name, no DB row → 0 rows deleted → no trigger → no broadcast).
    • Lifted optimistic cleanup into DashboardClient.cleanupDeletedDevice(name) (clears entries / statuses / infos / counts / filterDevices + IDB).
    • Plumbed onRemoved(name) through DeviceModal → DeviceTabs → DeviceSettingsTab so the cleanup fires after a successful API delete.
  • Rename duplicate-after-refresh fixed:
    • Broadcast UPDATE handler already remapped in-memory state — but every IDB-cached row still carried device: "<old-name>". Reload restored the stale-named entries into state before the live fetch arrived, so the device useMemo unioned both names.
    • New helper renameCachedEntriesForDevice(workspaceSlug, from, to) in app/lib/entriesCache.ts walks the workspace's rows with a cursor and rewrites the device field. Called from the rename handler.
  • Deploy script corrected from ssh ubuntu@p.bds.bot ... (OVH, decommissioning) to ssh ubuntu@busymate.net ... (DigitalOcean VPS, busymate-prod-lon1). The OVH path had been silently picking up every deploy since the rewrite, which is why dash.busymate.net was stuck on build 24 before today.

iOS — RealtimeSubscriber

Supabase Edge-Function-side advisor + the official supabase-swift Examples + open issues were the primary references this morning. Findings vs ours:

  • Channel-cache-by-topic gotcha is alive in supabase-swift too (RealtimeClientV2.swift:298-311 — cached instances win, ignoring the new options closure). teardown() now calls removeChannel(...) on all three channels before disconnect() so a re-bringUp() rebuilds fresh.
  • Redundant setAuth(token) before subscribe removed — the accessToken: { ... } closure in RealtimeClientOptions runs on every channel join. Canonical SlackClone pattern.
  • Redundant onBroadcast(event: "UPDATE") on devices:all removed — postgres_changes on devices filtered by uuid=eq.<self> on the per-device channel already covers it (also canonical).
  • Presence track() now gated on devicesAll.statusChange.filter({ $0 == .subscribed }). First event fires on the initial subscribe; every silent reconnect after a heartbeat drop re-publishes presence automatically. Replaces the brittle "track right after subscribeWithError" race.
  • gracefulStopForBackground() cancels the long-lived presenceTrackTask before untracking. Without that cancel, a silent reconnect during the bg suspend would re-fire the status-change observer and re-track — net effect was "device shows online even though backgrounded" (the regression we hit on build 28 and fixed on build 29).
  • retrackForForeground() simplified to always call bounce(). The previous "cheap retrack on the existing channel" path tried track() over the iOS-frozen-from-bg socket; the SDK still reported .subscribed, so frames silently went to /dev/null until Phoenix's heartbeat-timeout (~60 s) declared the socket gone and reconnected. A full bounce() finishes in 1-2 s and the new bringUp's status-change loop re-tracks on the first .subscribed. Net: bg → fg flips the dashboard back to Online within seconds.
  • vsn: .v1 pin reinstated (after a controlled-removal test). GitHub research said v2-sender frames should propagate to v1 subscribers because Phoenix selects a serializer per-connection. Empirically they don't on this Realtime project — the dashboard (supabase-js, vsn=1 default) saw presence from the proxy-server peers but never from the v2 iOS sender. Pin restored with the rationale documented inline citing https://supabase.com/docs/guides/realtime/protocol. Audit finding 030 (upstream docs gap) stays open.
  • SetupGuideView.nameHeader now renders v1.0 (N) as a .caption secondary monospaced-digits line below the editable device name, matching the dashboard.

Supabase

  • New migration 20260516000001_devices_self_insert_policy.sql:
    sql
    create policy devices_self_insert
      on public.devices
      for insert
      to authenticated
      with check (uuid = public.device_uuid());
    Without it, every iOS UPSERT against public.devices (writeDeviceSetup + writeDeviceName) silently 401'd. PostgREST gates UPSERT on the INSERT policy first, regardless of whether ON CONFLICT would trigger UPDATE. Symptom: "renaming from iOS does nothing" — the catch only logged at .debug so the failure was invisible in monitoring. Iphone now does propagate renames to the dashboard.

Infra

  • nginx mcp.busymate.net.conf hardened with resolver 1.1.1.1 8.8.8.8 valid=300s + variable indirection. At 2026-05-16 06:23 UTC a transient DNS failure for xfjplaganjqowkcnznbr.supabase.co killed nginx's config test during a certbot-triggered reload, taking the entire VPS offline. The variable-indirection form defers DNS resolution to request time so a startup-time DNS hiccup can't take everything down.
  • OVH box (bd-us, p.bds.bot) services stopped + disabled. The proxy-vps-bd-us presence row in the dashboard came from there — its busymate-proxy.service was registering its identity (via ~/.busymate-proxy/identity.json on that host) into Supabase. Both busymate-proxy and busymate-dashboard are now inactive + disabled on the OVH box. Droplet itself untouched.

Documentation + memory cleanup

  • SECURITY.md + README.md — every p.bds.bot reference replaced with proxy.busymate.net. README's component-status table now reads https://proxy.busymate.net for proxy-server (matches CLAUDE.md's canonical-hostnames section).
  • Memory filesreference_deno_deploy_env.md + reference_deno_deploy_token.md deleted (Deno Deploy gone post-rewrite). reference_mcp_busymate_logs.md rewritten to point to the mcp.busymate.net Edge Function endpoint + Bearer-via-supabase-secrets rotation flow. reference_stack.md updated to call out the OVH decommission and the new SSH target. MEMORY.md index trimmed.
  • New feedback memory: feedback_inc_before_deploy.md codifies the user's new rule — every new build / redeploy must FIRST bump build project-wide. Don't redeploy under an already-deployed build number, both so the version badge / BuildWatcher / TestFlight build number are unique identifiers AND so BuildWatcher's auto-reload functions.

Verified end-to-end

SurfaceTestStatus
dash.busymate.net/api/versionreachable pre-login, returns current build✅ build 31
nginx mcp.busymate.netsurvives DNS hiccup at reload✅ resolver-backed
iOS rename → dashboardSerebanSerebano → broadcast → dashboard updates✅ (probe + UI both saw)
Dashboard delete → stateRemove device on phantom row → disappears, stays gone after reload
bg → fg presencedashboard flips back to Online within seconds
Build numbersiOS pbxproj × 4 + version.json + 3 web package.json all match✅ all on 31

Build number

Bumped six times today: 24 → 25 → 26 → 27 → 28 → 29 → 30 → 31. Every redeploy got a fresh number per the new "inc-before-deploy" rule.