# Aesthetic Computer iOS — Progress Per-submission notes for the iOS app and iMessage extension in this directory. Append a new section on top for each App Store build. --- ## 1.2 (5) — 2026-10-15 Submitted at 20:12 UTC; App Store Connect reports `WAITING_FOR_REVIEW`. Build `06fc67b2-8c42-4d4d-b68d-e06ceb8efeb5`, submission `f60158d8-959c-4b5a-97b9-4bfa8ec8f93b`. The accompanying mail web change is deployed in `9973aecc01`. MIME creation and remix links now open the MIME app. If it is unavailable, creation falls back to mime.ac and remix falls back to the referenced work. The native bridge accepts these links only from the trusted main document. amail notifications retain their destination across cold launches and open the addressed letter. Only that letter is marked read. Existing APNs registration remains per device and bound to the signed-in account; `nonotifs` opts out this device. Foreground notifications now show a banner. Validation: iPhone Release archive and iOS Simulator build; Swift route checks; nine push bridge/target tests; 25 loopback APNs delivery checks; mail browser checks including targeted-letter and invalid-link cases. The signed IPA declares production APNs and disables get-task-allow. App and iMessage extension both carry version 1.2, build 5. Live APNs delivery on a physical device remains a TestFlight check. Compilation ran without credentials on a separate build machine. Before local App Store export, the archive was ad-hoc signed with its declared release entitlements; exporting an unsigned archive alone omitted APNs. The final exported signature was inspected, not inferred from the project. ## Shared Apple app — native Mac build, 2026-10-12 `AestheticComputerMac` now builds the same Swift sources as the iPhone/iPad target, with bundle ID `aesthetic.computer`. Run `./apple/install-mac.sh` to build the signed Intel/Apple Silicon app, install it in Applications, and pin it to the Dock. The installer retains the previous app and Dock preferences. Both platforms open `/prompt` with `nogap=true`, share recovery and the native music player, and accept `aesthetic://` piece links. Mac adds Command-L for the prompt, Command-R to reload, Command-O for audio files, and a title bar colored by the runtime's visible-buffer wipe color. Local Mac usage telemetry is off. Window titles follow the active piece; the application remains Aesthetic Computer. The window has no custom size floor (verified at 180 × 150) and joins Slab tiling, scatter, and directional navigation. Title-bar contrast is scoped to the native title bar, leaving the page's color scheme stable. The runtime reserves promotions for website visitors. Updates appear in the piece's top-left label as a faint letter glow and two tiny arrows (tap to refresh first), and in the prompt's bottom git-hash button. Native full-window prompt outlines are omitted so the macOS corner mask does not clip them. Separate top-right update controls are removed. Validation: signed universal Mac build, iOS device build without signing, route and usage-state checks, and runtime update/color-bridge tests. This is a local Mac installation; App Store submission and provisioned Mac push remain separate release work. Electron retains the other desktop integrations and Windows/Linux distribution. Mac warms the audio graph at boot, preserves the AC SVG cursor through native editor focus, and uses desktop keyboard shortcuts instead of Enter/Paste buttons. The unfocused prompt caret is hollow; the focused caret is solid. Prompt edits request an immediate render, coalesced behind any frame already in flight, without advancing simulation. Async bitmap uploads discard stale frames per canvas and release their GPU resources after drawing. Run `python3 apple/scripts/compare-prompt-latency.py` for paired native-app benchmarks at ordinary and held-key rates. It launches isolated, sandboxed instances of the installed app, sends real AppKit key events, checks all 60 characters arrive, and saves JSON timings plus canvas snapshots under `/tmp/ac-prompt-performance`. The baseline switch restores RAF-only input scheduling. Measurements cover DOM keydown through canvas submission and the next RAF opportunity, not physical keyboard or display latency. Normal app sessions do not enable the probe or record keystrokes. For a comparison with Terminal.app, build the external capture probe: ```sh xcrun swiftc -O -parse-as-library apple/scripts/compare-terminal-latency.swift -o /tmp/ac-visible-latency # Use dedicated EMPTY test prompts; PID and WindowServer ID must both match. # ROI is x,y,width,height in the captured image, covering only the first glyph. /tmp/ac-visible-latency PID WINDOW_ID /tmp/capture-preview 0 /tmp/ac-visible-latency PID WINDOW_ID /tmp/capture-run 60 X,Y,WIDTH,HEIGHT python3 apple/scripts/summarize-terminal-latency.py /tmp/ac-*/result.json /tmp/terminal-*/result.json ``` The capture probe injects only `a` and Backspace, checks window focus before measured keys, and detects a calibrated glyph mask in ScreenCaptureKit frames. Inspect `empty.png`, `typed.png`, and `mask.png` before trusting a run. Use the same window dimensions, alternate app order, and aggregate repeated runs. This endpoint is WindowServer's captured frame time, including native event delivery and composition. It excludes physical keyboard and panel latency and must not be compared directly with the DOM-to-canvas timings above. Capture cadence limits the measurement's resolution. The tool requires existing Accessibility and Screen Recording access and never requests it automatically. The [2026-10-12 comparison](benchmarks/2026-10-12-terminal/summary.json) recorded all 360 events across three runs per app at 348 × 279. Typing medians were AC 47 ms / Terminal 42 ms; Backspace medians were 49 / 35 ms. Capture cadence was about 34 ms, so treat small differences as approximate. Terminal used a clean `zsh -f` prompt. Raw samples accompany the summary. ## 1.1 (4) — 2026-09-11 (in progress) Builds 1.1 (2) and 1.1 (3) were uploaded in April and May but never attached to an App Store version, and TestFlight let them expire. This build carries everything both of them had plus the June push change, and it ships through a lane instead of by hand. ### Shipped - **Firebase removed.** Push tokens come straight from APNs (`didRegisterForRemoteNotificationsWithDeviceToken`) and reach the runtime as `kind: "apns"` via `/api/register-push-token`; the server fans out itself. No SDK, no GoogleService plist, smaller binary (commit `2a5816a281`). - **fastlane lane** at `apple/fastlane/` — the Menu Band / oskiewar shape: `fastlane ios meta` (creates the 1.1 version + metadata), `shots`, `build` (cloud-signed archive + App Store export to `apple/build/export/aesthetic.computer.ipa`), `upload`, `privacy`, and `ship`, which hands the last mile to `slab/menuband/bin/asc.mjs submit --app aestheticcomputer --platform IOS`. Metadata seeded from the live 1.0 listing in `fastlane/metadata/ios/en-US/`. ### Submission notes - `fastlane ios meta` ran 2026-09-11: version 1.1 created (PREPARE_FOR_SUBMISSION, id `c2212b58-deec-424e-a60d-5ac2ee3c7260`), description/keywords/URLs/What's New landed, and all eight screenshot sets (iPhone 6.7 + 5.5, iPad Pro 12.9 ×2, and their iMessage twins) were inherited from 1.0 — no `shots` run needed. - **Blocker 2026-09-11:** `fastlane ios build` fails at destination resolution — "iOS 26.5 is not installed. Please download and install the platform". Xcode 26.6 kept the iPhoneOS 26.5 SDK but has zero simulator runtimes, and Xcode 15+ needs the platform even for device archives. Fix is `xcodebuild -downloadPlatform iOS` (~8 GB on disk); the build lane now preflights this. - `CURRENT_PROJECT_VERSION` bumped 3 → 4 on both targets (3 was consumed by the expired upload). - Order: `fastlane ios meta` → `fastlane ios build` → `fastlane ios upload` → `node slab/menuband/bin/asc.mjs status --app aestheticcomputer --platform IOS` until build 4 reads VALID → `fastlane ios ship`. - What's New lives in `fastlane/metadata/ios/en-US/release_notes.txt`. --- ## 1.1 (3) — 2026-04-30 (in progress) Reliability pass before this build ships. Field reports of the app sitting on the boot.mjs animation indefinitely (5,000s+) with no way to recover short of force-quit. Root cause was structural — there was no host-side detection of a stuck JS runtime, no failure path on `WKNavigationDelegate`, and the network monitor cancelled itself on the first satisfied path so mid-session drops were invisible. ### Shipped - **Long-lived `AppNetworkMonitor`** (replaces the one-shot monitor in `ContentView`). Stays alive for the whole process and publishes online↔ offline transitions; flipping back online auto-reloads. - **Boot watchdog.** `BootStatus` runs a 5s-poll timer that flips `stalled = true` if no JS heartbeat has arrived for 25 seconds. Stalled state surfaces a SwiftUI overlay with a "Reload" button anchored at the bottom — covers the "stuck on boot animation" failure mode. - **`WKNavigationDelegate`** wired up. Provisional + final navigation failures populate `BootStatus.lastError` so the same overlay can show a proper error string ("Cannot reach aesthetic.computer — …") instead of a frozen UI. - **JS heartbeat** in `boot.mjs`. Every 1s during boot, then every 8s after `acHIDE_BOOT_LOG` fires, posts `{type:"boot:heartbeat"|"boot:ready"}` through the existing `iOSApp` message channel. Lets the host distinguish "slow load" from "deadlocked". - **Pull-to-refresh** on the WebView's scroll view (UIRefreshControl attached in `makeUIView`). Recovery is now one gesture. - **Scene-phase reload.** Returning the app from >5min of background triggers a fresh load — the WebView often holds a stale runtime (timed-out sockets, half-loaded modules) after long sleeps. - **`offline.html` retry button.** Posts `{type:"reload-online"}` to the host, which force-loads the live URL even if the path monitor still reads offline (cell handoffs lag a few seconds). - **Reload gating in `updateUIView`.** Tracks `lastLoadedKey` on the Coordinator so reloads only fire on real URL/trigger changes, not on every SwiftUI re-render (the previous code rebust+reloaded each time). ### Wiring summary - iOS: `apple/aesthetic.computer/ContentView.swift` — `AppNetworkMonitor`, `BootStatus`, watchdog, nav delegate, overlay, pull-to-refresh, scene phase. - iOS: `apple/aesthetic.computer/html/offline.html` — retry button. - Runtime: `system/public/aesthetic.computer/boot.mjs` — IIFE that posts `boot:heartbeat`/`boot:ready` via `webkit.messageHandlers.iOSApp`. ### Submission notes - Same bundle IDs / team / Apple ID as 1.1 (2). - Bump `CFBundleVersion` to `3` before archiving. - What's New text: "Recovery from hung loads — reload from a stuck boot screen, automatic retry when the network reconnects, pull down to refresh." --- ## 1.1 (2) — 2026-04-23 First update since 1.0 (1) shipped as RC4 in late 2024. Focus: per-device push notifications, replacing the slow topic-broadcast model. ### Shipped - **FCM device-token bridge.** `MessagingDelegate` hands the token to the WebView via `evaluateJavaScript(window.iOSReceivePushToken(...))` with retry until the AC runtime is ready. The runtime POSTs `/api/register-push-token` against the logged-in user so the backend can target individual devices. - **`tell` command.** `tell @handle message` from the AC prompt calls `/api/tell`, which writes to the `tells` Mongo collection and sends a direct-token FCM push (sub-second latency vs. minutes for topic broadcasts). Deliberately one-way for v1 — no reply UI, no thread. - **Firebase iOS SDK 10.19 → 11.15.** 10.19's prebuilt xcframeworks were missing `CFBundleShortVersionString` / `MinimumOSVersion` in their Info.plists, which Apple's submission validator began rejecting in late 2024. Dropped the now-removed `FirebaseAnalyticsSwift` product (merged into `FirebaseAnalytics` in 11.x). - **Account-deletion cascade.** `delete-erase-and-forget-me` now clears `push-tokens` and `tells` alongside existing `moods` cleanup. ### Wiring summary - iOS: `apple/aesthetic.computer/aesthetic_computerApp.swift` — `deliverPushTokenToWebView` - Runtime: `system/public/aesthetic.computer/bios.mjs` — `window.iOSReceivePushToken`, `iOSTryRegisterPushToken`, `iOSUnregisterPushToken` - Runtime: `system/public/aesthetic.computer/boot.mjs` — login-drain + logout-unregister hooks - Backend: `system/netlify/functions/register-push-token.mjs`, `tell.mjs` - Prompt: `system/public/aesthetic.computer/disks/prompt.mjs` (`tell` handler) ### Build/submission notes - Bundle IDs: `aesthetic.computer` (main), `aesthetic.computer.aesthetic` (iMessage ext). - Team: `FB5948YR3S`. Apple ID for distribution: `me@jas.life`. - App-specific password lives at `aesthetic-computer-vault/apple/app-specific-password.env.gpg`. - CLI archive + export + upload works via `xcodebuild archive -allowProvisioningUpdates` → `xcodebuild -exportArchive` → `xcrun altool --upload-app`. See `/tmp/ac-ios-*.log` from this session for reference commands. - Submit-for-review (What's New, export compliance, etc.) still manual in App Store Connect — requires an ASC API key `.p8` to automate. Drop one in `aesthetic-computer-vault/apple/appstoreconnect/` next time to close the loop. ### Known gaps / candidates for 1.2 - `tells` inbox piece — right now the push notification *is* the UI. A `tells.mjs` piece + deep-link from the notification tap-through (Swift already forwards `userInfo["piece"]` → `iOSAppSwitchPiece`) would show the conversation log. - `mood` notifications still go via FCM topic (`mood.mjs:346`). Convert to per-follower direct sends once a follower graph exists. - App Store screenshots are stale — proposal: extend `oven/` with an `app-shots.mjs` driving `xcrun simctl` across the required device sizes to auto-capture a curated set of pieces. - ASC API key for unattended "Submit for Review" (see above).