feat: add HomeSec iOS app shell MVP#105
Draft
lan17 wants to merge 36 commits into
Draft
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #105 +/- ##
==========================================
+ Coverage 87.16% 87.20% +0.04%
==========================================
Files 151 155 +4
Lines 16422 16927 +505
==========================================
+ Hits 14314 14761 +447
- Misses 2108 2166 +58 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What This PR Builds
This PR turns the existing HomeSec React UI into a latest-iOS native app while keeping the server as the system of record. The native app is a Capacitor shell under
ui/ios, and the React app continues to own setup, routing, live view, clips, event detail, settings, and media workflows. FastAPI continues to own API contracts, auth, camera control, clip metadata, preview/talk transport, mobile-device registration, and APNs delivery.The main architectural boundary is intentionally simple: Swift handles iOS-only capabilities that browser JavaScript cannot safely provide, while React and the existing backend remain the product and data layers. Native code is limited to Keychain storage, device metadata, APNs registration plumbing, URL-scheme/deep-link entry points, local-network permission declarations, and AppDelegate handoff to Capacitor.
Overall Design and Architecture
flowchart LR subgraph IOS["iOS App"] Shell["Capacitor Shell / WKWebView"] React["React HomeSec App"] AuthPlugin["HomeSecAuth Swift Plugin<br/>Keychain server URL + API token"] DevicePlugin["HomeSecDevice Swift Plugin<br/>bundle, app, APNs environment"] PushPlugin["Capacitor Push Notifications"] DeepLinks["homesec:// and notification tap routing"] end subgraph Server["HomeSec FastAPI Server"] Auth["Existing Bearer-token auth"] ExistingAPI["Existing operational APIs<br/>health, cameras, clips, preview, talk, stats"] MobileAPI["Mobile device API<br/>/api/v1/mobile/devices"] Repo["MobileDeviceRepository"] end subgraph Data["Postgres"] Devices["mobile_devices"] ClipData["clip state and events"] end subgraph Delivery["Notification Delivery"] Notifier["apns_mobile notifier plugin"] APNs["Apple APNs HTTP/2"] end Shell --> React React <--> AuthPlugin React --> DevicePlugin React --> PushPlugin PushPlugin --> React DeepLinks --> React React --> Auth Auth --> ExistingAPI Auth --> MobileAPI MobileAPI --> Repo Repo --> Devices ExistingAPI --> ClipData Notifier --> Repo Notifier --> APNsThe native setup path hydrates runtime API providers before React routing performs authenticated queries. Browser mode keeps the web app's existing same-origin behavior. iOS native mode reads the server URL and auth state from Keychain; if either value is missing, the app redirects to
/native-setupand validates the server before persisting credentials.sequenceDiagram participant User participant App as iOS App / React participant Keychain as HomeSecAuth Plugin participant API as HomeSec Server participant APNs as APNs Registration App->>Keychain: Read server URL and auth readiness alt Missing native setup App->>User: Show native setup User->>App: Enter server URL App->>API: GET /api/v1/health API-->>App: Health response User->>App: Enter API token App->>API: GET /api/v1/cameras with Bearer token API-->>App: Token accepted App->>Keychain: Store server URL and token end App->>API: Load live, event, clip, and settings data opt Native push enabled App->>APNs: Request permission and APNs token App->>API: POST /api/v1/mobile/devices API-->>App: Redacted device record endServer-Side Changes
The server-side work adds the minimum persistent identity and delivery layer needed for APNs without making the recording path depend on mobile availability. Recording and upload remain P0; mobile notifications are best-effort and can fail without blocking clip processing.
The
mobile_devicestable is created by one consolidated Alembic migration in this PR. It stores the APNs token hash for dedupe, the raw APNs token for the internal sender path, APNs environment, bundle id, device metadata, JSONB capabilities, soft-disable state, and latest delivery status. The earlier split migration was folded back because this is still an unmerged PR, so there is no deployed migration boundary to preserve.The mobile API exposes registration, list, update, and disable operations at
/api/v1/mobile/devices. The stale/api/v1/mobile/notifications/teststub was removed during review because APNs delivery now exists as a real notifier plugin and a fake API surface would create the wrong contract.APNs delivery is implemented as the normal
apns_mobilenotifier backend. It filters targets by bundle id and APNs environment, signs Apple provider tokens with ES256, sends alert pushes through APNs HTTP/2, records per-device delivery status, and disables devices when APNs returns permanent token rejections such asBadDeviceToken,DeviceTokenNotForTopic,Unregistered, or HTTP410.The review loop also tightened retry semantics. APNs fanout now reports whether a failure is retryable. The pipeline honors that signal so a partially successful fanout does not retry and duplicate notifications for devices that already accepted the push. If every delivery failed for retryable reasons, the normal pipeline retry path can still run.
iOS and Web App Changes
The iOS app targets latest iOS only. The project, signing settings, Info.plist declarations, Swift Package sync output, AppDelegate notification handoff, and CI native build are all committed so the shell can be built from a clean checkout.
The React runtime gained server-base-url and token provider abstractions so the same app can run in browser mode or iOS native mode. The native setup flow validates
/api/v1/health, validates the token through/api/v1/cameras, stores credentials in Keychain, and clears query state when switching servers. HTTP setup URLs are constrained to local/private hosts; remote deployments should use HTTPS or VPN.Native bridges are deliberately narrow.
HomeSecAuthowns Keychain-backed server URL, API token, and auth-disabled readiness. It now preserves non-root server paths when normalizing base URLs.HomeSecDevicereports bundle id, app version, device name, and the APNs environment from the app's build configuration.AppDelegatekeeps only the Capacitor URL/open and remote-notification handoff code needed by this app.The React app also gained native lifecycle and routing behavior: live preview and push-to-talk stop when the app backgrounds, APNs taps and
homesec://links route through React Router with safe setup/auth fallbacks, and event detail views handle deleted or media-unavailable notification targets cleanly.CI, Testing, and Deployment
CI now includes a native iOS simulator build on
macos-26. The job installs UI dependencies, runspnpm --dir ui ios:sync, verifies the committed iOS sync output, and builds the simulator target with code signing disabled. Usingmacos-26is intentional because GitHub'smacos-latestmigration to macOS 26 starts on June 15, 2026 and completes on July 15, 2026; today, June 14, 2026,macos-latestcan still resolve to an older image.The installed app can point at any reachable HomeSec server that exposes the existing web UI API contracts. Core app use needs unauthenticated
GET /api/v1/healthand authenticated operational APIs such asGET /api/v1/cameras, clips, preview, talk, stats, and settings. APNs registration and push delivery require this PR's server schema and mobile routes to be deployed.For LAN device testing, the server should bind to an address the phone can reach:
Personal Apple development teams can install and launch the app with local signing adjustments, but they cannot provision Push Notifications. Full APNs QA requires a team/profile with the push entitlement.
Ticket Traceability
Epic: HomeSec iOS/iPad app
Review Loop Outcome
The latest review loop focused on migration hygiene, APNs correctness, runtime boundaries, native setup correctness, CI coverage, and dead code. The fixes from that pass consolidated the mobile schema into one migration, removed the stale notification-test endpoint, renamed the misleading
apns_token_encryptedcolumn toapns_token, prevented duplicate pushes after partial APNs fanout failures, disabled permanently invalid APNs devices, skippedapns_mobilecleanly when runtime Postgres state is unavailable, switched native push registration to the configured runtime API client, allowed denied-then-granted notification registration to retry, preserved pathful server base URLs in Keychain, made APNs environment derive from Info.plist, pinned native CI tomacos-26, and removed unused AppDelegate lifecycle template methods.Remaining constraints are operational. APNs end-to-end delivery still needs a provisioning profile with the Push Notifications entitlement, and real-server mobile registration needs this PR's backend schema/routes deployed. The core app can still be used against a server that has the existing web UI APIs.
Validation
Latest pushed head:
dccfc32.Validation performed on the final review-loop head:
PATH=/Users/levneiman/.nvm/versions/node/v22.22.0/bin:$PATH TEST_DB_DSN=postgresql://homesec:homesec@localhost:15433/homesec make check1411 passed, 2 warnings.70test files /386tests passed.PATH=/Users/levneiman/.nvm/versions/node/v22.22.0/bin:$PATH pnpm --dir ui ios:syncpassed and kept committed native sync output stable.PATH=/Users/levneiman/.nvm/versions/node/v22.22.0/bin:$PATH xcodebuild -project ui/ios/App/App.xcodeproj -scheme App -destination 'generic/platform=iOS Simulator' -configuration Debug CODE_SIGNING_ALLOWED=NO buildpassed against the installed Xcode/iOS Simulator SDK.git diff --check -- . ':(exclude)graphify-out'andgit diff --cached --checkpassed.Previous validation on earlier PR heads included Playwright browser/mobile runs, simulator launch smoke on iPhone/iPad iOS 26.5 targets, local physical-device launch smoke on an iPhone 14 Pro Max running iOS 26.5, focused APNs/mobile API/backend tests, and native setup/deep-link/lifecycle UI tests.
Notes
The worktree still has unrelated generated wiki edits in
graphify-out/wiki/FilterResult.mdandgraphify-out/wiki/RTSPSource.md. They were intentionally excluded from the commit and this PR.