The console now ships the way the app and the Manager do: every push touching admin/ builds an unsigned IPA and publishes it, together with a generated AltStore source, to a rolling `admin-dev` prerelease. - admin/app-repo.json is the source template the workflow rewrites with jq, same as manager/app-repo.json. - The tag is `admin-dev`, distinct from `dev` and `manager-dev` so the three builds don't overwrite each other's assets. - admin-build.yml is gone; the dev workflow covers the compile check it existed for, and running both would build every admin/ push twice. The repo is private, so "published" here means reachable by whoever already has access — which is the whole distribution model for an operator console. There is still no web/ proxy and no in-app updater: this app only ever calls the session-gated /api/* routes.
5.4 KiB
FTStore Admin — project guide for AI agents
FTStore Admin is the 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 first — it owns the tag namespace (
admin-v*) and the rule thatscripts/apply-rebrand.shmust never touch this directory. This app was split out ofmanager/(where it was a hidden second tab); the git history ofConsole/*.swiftruns back throughmanager/FTStore/Admin/.
Brand / identity
- App name
FTStore Admin· bundle idapp.ftstore.admin· target/scheme/productFTStoreAdmin. - The server in
web/is also called FTStore Admin. In prose, say "the FTStore Admin server" forweb/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/andmanager/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
NimbleKitor the FTStore engine, inline it inSupport/with a note on where it came from (that is whatAdminUIKit.swiftandAnyCodable.swiftare). Adding a package dependency is a decision, not a convenience. AdminSessionis the single door. Every screen callssession.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 callAdminAPIdirectly from a view.- The token is Keychain-only (
AdminKeychain, serviceapp.ftstore.admin,AfterFirstUnlockThisDeviceOnly) and the app'sURLSessionis ephemeral withhttpShouldSetCookies = 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 aweb/route this app calls, wrap it — middleware only checks that a cookie exists, not that it is valid. AdminModels.swiftmirrors the handlers' responses and holds every request body inAdminRequest; 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 throughAdminAPI.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_SIZEinweb/src/lib/pipeline/upload-staging.ts) with a lowercase-UUIDuploadId— 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 ownrenameScopeto/api/library/apply, never an edit to the entry row. - Sheets get
.environmentObject(session)explicitly — a missing@EnvironmentObjectis a crash, not a nil. - The User-Agent (
FTStore-Admin/…) deliberately does not matchweb/'sAPP_UAregex. 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 intoInfo.plist(FTStoreAdminBaseURL). Never commit a real credential.app-repo.json— the AltStore source template the dev workflow rewrites withjq. Itsversion/downloadURLare placeholders for a stableadmin-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):
plistlibparse ofFTStoreAdmin/Resources/Info.plist, and grep this directory forghp_/ any credential — there must be none.
GPL-3.0
Keep the root LICENSE and the attributions intact; keep source public.