Files
FTStore/admin/AGENTS.md
T
Felitendo ffffbf7f2e feat: add the feedback board
Bug reports and feature requests written inside FTStore arrive in the console
and the dashboard, where they can be answered, ranked, moved through a status,
taken down or muted.

Settings lost the parts that were never really settings. The store's name,
subtitle, description, icon and tint are fixed now, because this server
publishes one store and renaming it only ever broke the link for people who had
already added it. The GitHub owner, repo and branch fields are gone as well: the
app repository is fixed, and every scanned repository is configured per source.
The GitHub token stays, it is still the fallback the update proxy falls back on.

IPA links are only ever answered for the FTStore app itself, so the switch that
relaxed that is gone, and with it the action that copied a download link, which
could only lead somewhere that refuses it.

Channels and Sources in the console app gained the per-topic switches the
dashboard already had, so a forum can be scanned selectively from a phone.
Choosing an .ipa to upload works there too: the file type is declared now, which
is what the document browser wanted before it would let one be picked.
2026-08-05 23:42:15 +02:00

6.5 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.
  • English and German, chosen by iOS. Resources/Localizable.xcstrings holds both, keyed by the English source string, and de is in the project's knownRegions. A literal passed to Text/Button/Label/Section/.navigationTitle/.alert is a LocalizedStringKey and looks itself up; a String built before it reaches a view does not, so that is what String.localized(…) in Support/AdminLocalization.swift is for. The shared components in AdminComponents.swift localize their own String labels, which is why call sites keep passing plain English. A missing German entry renders the English key. Never translate text the server sent.
  • The .ipa file importer needs the type declared. Info.plist imports app.ftstore.ipa (the identifier ios/ exports) because UTType(filenameExtension:) answers with a dynamic dyn.… type when nothing on the device has claimed the extension, and the document browser then greys out every IPA. UTType.ipaImportTypes widens the picker further (.zip, .data) for files tagged some other way, and AdminUploadView._pick is what enforces .ipa.
  • 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.