# FTStore Admin — project guide for AI agents FTStore Admin is the [`web/`](../web) dashboard as an iOS app: the operator console for the FTStore Admin server, behind the same login as the web UI. > **Monorepo.** Read the root [AGENTS.md](../AGENTS.md) first — it owns the tag namespace > (`admin-v*`) and the rule that `scripts/apply-rebrand.sh` must never touch this > directory. This app was split out of [`manager/`](../manager) (where it was a hidden > second tab); the git history of `Console/*.swift` runs back through > `manager/FTStore/Admin/`. ## Brand / identity - App name `FTStore Admin` · bundle id **`app.ftstore.admin`** · target/scheme/product `FTStoreAdmin`. - The *server* in `web/` is also called FTStore Admin. In prose, say "the FTStore Admin server" for `web/` and "the FTStore Admin app" for this one when both are in play. ## The rules that hold it together - **Nothing secret ships in the app.** The only baked-in value is the base URL. Never add a token, key or privileged path — the whole point is that a decrypted IPA is a login form and nothing else. - **No operator surface leaks back.** This app exists so `ios/` and `manager/` never need one. Don't add a dashboard feature to either of them, not even behind a hidden gate; that gate is what was removed here. - **No Swift packages.** The target links system frameworks only. If something needs a helper from `NimbleKit` or the FTStore engine, inline it in `Support/` with a note on where it came from (that is what `AdminUIKit.swift` and `AnyCodable.swift` are). Adding a package dependency is a decision, not a convenience. - **`AdminSession` is the single door.** Every screen calls `session.get/send/act`; that is the one place that attaches the token and the one place that reacts to a 401 (wipe the local session, drop back to the login form). Don't call `AdminAPI` directly from a view. - **The token is Keychain-only** (`AdminKeychain`, service `app.ftstore.admin`, `AfterFirstUnlockThisDeviceOnly`) and the app's `URLSession` is ephemeral with `httpShouldSetCookies = false`, so it is never written to the container. The password is used for one login request and dropped. - **Server-side auth is the real gate.** `withAuth()`/`withAdmin()` on every non-public route is what protects the data; hiding the Users link from non-admins is presentation. If you add a `web/` route this app calls, wrap it — middleware only checks that a cookie *exists*, not that it is valid. - `AdminModels.swift` mirrors the handlers' responses and holds every request body in `AdminRequest`; keep the two in step when a route changes shape. - Two endpoints don't take JSON — the chunked IPA upload and the tweak icon. They go through `AdminAPI.upload` (multipart); the settings backup goes through `AdminAPI.rawData`, because it is a document that has to round-trip byte-for-byte rather than a model to decode and re-encode. - The IPA upload must keep posting **8 MiB chunks in order** (`UPLOAD_CHUNK_SIZE` in `web/src/lib/pipeline/upload-staging.ts`) with a lowercase-UUID `uploadId` — the server validates that id against a strict pattern before it becomes a file path. Chunk reads run on a detached task so an 8 MiB read can't stutter the progress bar. - A Library rename targets a *variant group*, not a build: send `groupKey` + the item's own `renameScope` to `/api/library/apply`, never an edit to the entry row. - Sheets get `.environmentObject(session)` explicitly — a missing `@EnvironmentObject` is a crash, not a nil. - The User-Agent (`FTStore-Admin/…`) deliberately does **not** match `web/`'s `APP_UA` regex. The console only calls session-authenticated routes, so it never needs the feed's UA gate — and keeping it outside means nobody widens that gate to make this app work. ## Where things live - `FTStoreAdmin/FTStoreAdminApp.swift` — the `@main`; owns the session and the re-lock on backgrounding. - `FTStoreAdmin/Console/*` — transport, session, models, one view per dashboard screen. - `FTStoreAdmin/Support/*` — base URL/User-Agent, brand colour, the inlined helpers. - `FTStoreAdmin.xcconfig` (`FTSTORE_PRODUCT_BUNDLE_IDENTIFIER`, `FTSTORE_ADMIN_BASE_URL`) → injected into `Info.plist` (`FTStoreAdminBaseURL`). **Never commit a real credential.** - `app-repo.json` — the AltStore source template the dev workflow rewrites with `jq`. Its `version`/`downloadURL` are placeholders for a stable `admin-v*` release; the dev build overwrites every one of those fields. ## Distribution Same shape as the app and the Manager: `admin-dev-repo.yml` builds every push touching `admin/` and publishes the IPA plus a generated AltStore source (`admin-devrepo.json`, from `admin/app-repo.json`) to the rolling **`admin-dev`** prerelease. `admin-release.yml` cuts a draft release from an `admin-v*` tag when a stable one is wanted. The repo is private, so that source is reachable only by someone who already has access — which is the point. There is still **no `web/` proxy and no in-app updater** for this app: it talks to the session-gated `/api/*` routes, never to `/api/ftstore-*`. ## Build / validate - macOS + Xcode only. `make` → `packages/FTStore-Admin.ipa`. - Static checks (CI/Linux ok): `plistlib` parse of `FTStoreAdmin/Resources/Info.plist`, and **grep this directory for `ghp_` / any credential — there must be none.** ## GPL-3.0 Keep the root `LICENSE` and the attributions intact; keep source public.