Files
FTStore/admin/AGENTS.md
T
Felitendo 96547ac86f ci(admin): publish a rolling admin-dev prerelease instead of build artifacts
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.
2026-08-04 09:29:58 +02:00

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 that scripts/apply-rebrand.sh must never touch this directory. This app was split out of 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.