Skip to content

feat: add HomeSec iOS app shell MVP#105

Draft
lan17 wants to merge 36 commits into
mainfrom
codex/homesec-ios-m1
Draft

feat: add HomeSec iOS app shell MVP#105
lan17 wants to merge 36 commits into
mainfrom
codex/homesec-ios-m1

Conversation

@lan17

@lan17 lan17 commented May 11, 2026

Copy link
Copy Markdown
Owner

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 --> APNs
Loading

The 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-setup and 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
    end
Loading

Server-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_devices table 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/test stub 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_mobile notifier 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 as BadDeviceToken, DeviceTokenNotForTopic, Unregistered, or HTTP 410.

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. HomeSecAuth owns Keychain-backed server URL, API token, and auth-disabled readiness. It now preserves non-root server paths when normalizing base URLs. HomeSecDevice reports bundle id, app version, device name, and the APNs environment from the app's build configuration. AppDelegate keeps 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, runs pnpm --dir ui ios:sync, verifies the committed iOS sync output, and builds the simulator target with code signing disabled. Using macos-26 is intentional because GitHub's macos-latest migration to macOS 26 starts on June 15, 2026 and completes on July 15, 2026; today, June 14, 2026, macos-latest can 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/health and authenticated operational APIs such as GET /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:

server:
  enabled: true
  host: "0.0.0.0"
  port: 8081
  auth_enabled: true
  api_key_env: HOMESEC_API_KEY

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

Area Tickets
Design and docs iOS-00, iOS-20
API runtime and native setup iOS-01, iOS-02, iOS-03
Capacitor shell and native runtime iOS-04, iOS-05, iOS-06, iOS-07
Lifecycle, links, and layout iOS-08, iOS-09, iOS-10
Preview and notification UX iOS-11, iOS-12, iOS-18
Mobile device backend and APNs iOS-13, iOS-14, iOS-15, iOS-16, iOS-17
QA iOS-19

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_encrypted column to apns_token, prevented duplicate pushes after partial APNs fanout failures, disabled permanently invalid APNs devices, skipped apns_mobile cleanly 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 to macos-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 check
    • Backend lock/lint/format/strict typecheck passed.
    • Backend tests: 1411 passed, 2 warnings.
    • UI API/lint/typecheck/build passed.
    • UI tests: 70 test files / 386 tests passed.
  • PATH=/Users/levneiman/.nvm/versions/node/v22.22.0/bin:$PATH pnpm --dir ui ios:sync passed 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 build passed against the installed Xcode/iOS Simulator SDK.
  • git diff --check -- . ':(exclude)graphify-out' and git diff --cached --check passed.

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.md and graphify-out/wiki/RTSPSource.md. They were intentionally excluded from the commit and this PR.

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.47656% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.20%. Comparing base (0d677d9) to head (dccfc32).

Files with missing lines Patch % Lines
src/homesec/plugins/notifiers/apns_mobile.py 83.82% 38 Missing ⚠️
src/homesec/repository/mobile_device_repository.py 89.88% 9 Missing ⚠️
src/homesec/app.py 27.27% 8 Missing ⚠️
src/homesec/runtime/worker.py 83.33% 3 Missing ⚠️
src/homesec/api/routes/mobile.py 98.48% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant