Records D-004 and D-015 to D-025: shells and ownership, server seams (router middleware, rate_limit layer, tasks::start, AuditWriter and PushQueue traits, next_occurrence_date in kvitt-domain, M3 owns recurring generation, M4 lands after M3), OpenAPI parsed-value equality, feature convention and strings, empty fake scenario and resolveRange exception, shared ledger rows and editor fields in :core:design, no bundled font, pruning at 04:00, development infrastructure as M2c-dev built it, M2c-pi allowances, release keystore, production import procedure. Plans and prompts close their open points; HANDOFF rewritten to the real state; CLAUDE.md: implementation agents never edit SPEC.md or docs/decisions.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
PLAN-M2c-dev: Infra for development (D-001, D-022)
On record: landed on main as c885aba. As built (SPEC v0.3 §10 intro, §10.7, D-022): no Caddy, no TLS; bind-mounted data in infra/dev/data/; infra/secrets/ mounted read-only at /secrets; env file infra/dev/.env.example; dev scripts infra/dev/{build,up,logs,backup,copy-google-services}.sh; runbook in infra/README.md (not infra/dev/README.md); infra/env.example is left to M2c-pi. Firebase resources exist. Where this plan and SPEC differ, SPEC wins.
1. Goal
Give development a local server on the owner's M4 Mac: a Docker compose stack with the server (built from server/Dockerfile), optional Caddy, and an on-demand backup job, reachable by debug builds on the owner's phone over the home Wi-Fi. Set up the Firebase project and apps now, because push is needed during development.
2. Read first
- SPEC §10 Deployment & ops: the intro paragraph "Development first (D-001)", §10.2 Compose stack (service shapes, limits, logging), §10.3 Images, registry and build (the
serverandbackupimages), §10.4 Server configuration and CLI, §10.7 Firebase setup, §10.9 Backups and restore (backup steps 1 to 5). - SPEC §4 AC-2.7 and AC-2.8 (debug builds accept
http://to private LAN addresses), FR-20 Health. - SPEC §8.7 Privacy and logging, §8.9 Server hardening (container user, read-only root, pragmas).
- SPEC §11.2 (row M2c-dev), §11.3 (note M2c-dev / M2c-pi), §12 (OQ-1 answered, OQ-3 open for M2c-pi only), §14.7.
docs/decisions.mdD-001, D-002, D-022.- Files:
server/Dockerfile(from M2a, if landed),.gitignore,CLAUDE.md(BUILD AND VERIFY: Cargo and Docker builds; EXECUTION PROTOCOLS: Stay inside the repository, Stop and ask; THE OWNER).
3. Already established
- During development the server runs on the Mac, via
infra/dev/compose.yamlor plaincargo run; debug builds pair against it (D-001, §10 intro). - The dev stack has
server, an optionalcaddywith a self-signed certificate or no TLS for local use, andbackuprunnable on demand without the Drive upload (D-001, §10 intro, §11.2). backup.shsteps:.backupsnapshot,PRAGMA integrity_checkmust printok, gzip and pictures tar with a UTC stamp, rclone copy only,last-successmarker; whenRCLONE_FOLDER_IDis empty step 4 is skipped with a log line and local files are kept (§10.9, D-001). On the Pi, local files are deleted as soon as confirmed on Drive (D-002; built by M2c-pi).backupimage:alpinewithsqlite,gzip,tar,rclone,infra/backup/backup.shrun bycrond(§10.3).- Server config is only the env variables of §10.4;
KVITT_FCM_CREDENTIALSdefault/secrets/fcm-service-account.json, push disabled with a warning when missing;KVITT_FCM_PROJECT_IDkvitt-1337(§10.4). - Firebase (D-022): no new CLI installs;
gcloudexists, the Firebase CLI is not used; the Firebase project and the Android apps are created through the Firebase Management REST API withgcloud auth print-access-token;rclonebelongs to M2c-pi. Projectkvitt-1337; Android appsse.hagstedt.kvitt("Kvitt") andse.hagstedt.kvitt.debug("Kvitt debug"); one mergedgoogle-services.jsonininfra/secrets/(git-ignored), which M8 copies intoandroid/app/withinfra/dev/copy-google-services.sh; service accountkvitt-fcmwithroles/firebasecloudmessaging.admin, key atinfra/secrets/fcm-service-account.json; stop if an organisation policy blocks key creation; free tier, no billing (§10.7). - The registry is Gitea at
http://gitea.lan; not used by this plan (D-001, OQ-1). - Secrets live in
infra/secrets/(git-ignored) and are never committed (§10 intro,.gitignore). - Never build Docker images while a Gradle build runs (CLAUDE.md).
4. Owned paths
From SPEC §14.7, verbatim: infra/** (owner "M2c-dev, then M2c-pi (D-001)"; after landing: "M10 updates runbooks").
Also (D-022): the git-ignored contents of infra/secrets/ (fcm-service-account.json, google-services.json) written in the main checkout /Users/jhag/projects/kvitt (where every worktree copies them from, CLAUDE.md), and actions on the Google Cloud project kvitt-1337 for §10.7 steps 2 to 6. Nothing else outside the repository; android/app/ is not touched (M8 runs the copy script).
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
5. Deliverables
infra/dev/compose.yaml:server(image built fromserver/Dockerfile, data in a named volume, FCM key mounted read-only frominfra/secrets/,KVITT_PUBLIC_URLset to the Mac's LAN URL, user 10001, read-only root, no capabilities,no-new-privileges), optionalcaddy,backup(on demand), logging andTZ=Europe/Stockholmas §10.2.infra/dev/Caddyfile: only if the stack includes Caddy (self-signed viatls internal, or plain HTTP).infra/dev/README.md: start, stop, logs,bootstrapandpair-devicecommands, finding the Mac's LAN address, pairing a debug build over the home Wi-Fi, running a backup, resetting the dev data.infra/backup/Dockerfile,infra/backup/backup.sh: the backup image and script of §10.9 with the empty-RCLONE_FOLDER_IDlocal mode.infra/dev/.env.example: the variablesinfra/dev/compose.yamlreads, with dev-friendly values (no secrets); the gate runsconfigwith it.infra/env.example: the.envkeys of §10.2 with comments (no secret values).infra/dev/copy-google-services.sh: copiesinfra/secrets/google-services.jsontoandroid/app/google-services.jsonof the checkout it runs in (exits non-zero with a message when the source is missing); shellcheck clean.- Firebase project
kvitt-1337with both Android apps (cloud state, no repository file). infra/secrets/fcm-service-account.json(git-ignored, main checkout).infra/secrets/google-services.jsonwith both clients (git-ignored, main checkout).
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m2c-dev -b agent-m2c-dev main, then in.claude/worktrees/agent-m2c-dev:git reset --hard main. Copy from the main checkout whatever already exists ofinfra/secrets/(mkdir -p infra/secrets && cp -R /Users/jhag/projects/kvitt/infra/secrets/. infra/secrets/if present). Runtools/install-git-hooks.sh. - Read §2 in order. Check the tools:
command -v docker gcloud shellcheck curl,docker buildx version,docker compose version. No new CLI is installed (D-022). Ifdockerorgcloudis missing, stop and ask the owner (do not install). - Versions: resolve from official sources and record: the
alpineimage tag and digest (Docker Hub official image),rclonepackage version in that Alpine release, thecaddyimage version and digest if Caddy is used (Docker Hub official image). Pin by version and digest. - Write
infra/backup/backup.shandinfra/backup/Dockerfile; runshellcheckon the script. - Write
infra/dev/compose.yaml(andinfra/dev/Caddyfileif used),infra/env.example,infra/dev/README.md. Record which TLS option (self-signed or none) was chosen and why. - Run
docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config. - Firebase (needs HUMAN step H-1, §8). After the owner signed in, run SPEC §10.7 steps 2 to 6 (v0.3):
gcloud config set project kvitt-1337;gcloud services enable fcm.googleapis.com firebase.googleapis.com; then withTOKEN=$(gcloud auth print-access-token)call the Firebase Management REST API (https://firebase.googleapis.com/v1beta1/; confirm the requests, the long-running operation polling and any quota-project header from Google's official API reference):projects.addFirebaseforprojects/kvitt-1337(skip if already a Firebase project),projects.androidApps.createfor "Kvitt" /se.hagstedt.kvittand "Kvitt debug" /se.hagstedt.kvitt.debug(skip an app that already exists),projects.androidApps.getConfigfor each, merged into one/Users/jhag/projects/kvitt/infra/secrets/google-services.jsonholding both clients. Never print the config or the token. Thengcloud iam service-accounts create kvitt-fcm --display-name "Kvitt FCM sender"; grantroles/firebasecloudmessaging.adminon the project;gcloud iam service-accounts keys create /Users/jhag/projects/kvitt/infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com. If an organisation policy blocks key creation, stop and report; change no policy. Copying the key to the Pi (§10.7 step 7) belongs to M2c-pi. - Health check (needs M2a on
main; if not landed, report this part as outstanding): build the server image locally (no Gradle build running),docker compose -f infra/dev/compose.yaml up -d,curl -fsS http://localhost:<port>/healthon the Mac; then the owner's phone check (§8 H-2), relayed by the orchestrator. - Backup check: run the backup job once on demand (
docker compose -f infra/dev/compose.yaml run --rm backup /backup.shor as written in the README), thensqlite3 <snapshot> "PRAGMA integrity_check"printsok. - Run
shellcheck $(find infra -name '*.sh'). - Run the gate (§7). Commit on
agent-m2c-dev(never a secret),git rebase main, rerun the gate. Never merge, push or remove the worktree. Leave the dev stack running or stopped as the start prompt says, and say which in the report.
7. Gate
From SPEC §11.2 (v0.3), verbatim: "docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config succeeds; shellcheck $(find infra -name '*.sh') clean; on the Mac (with the M2a image): GET /health answers 200 from the Mac; one local backup run produces a snapshot that passes PRAGMA integrity_check".
Commands: docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config; shellcheck $(find infra -name '*.sh'); curl -fsS http://localhost:<port>/health; sqlite3 <snapshot> "PRAGMA integrity_check".
Green means: config exits 0; shellcheck prints nothing and exits 0 (if shellcheck is not installed, stop and ask; do not install it); /health returns 200 with "status":"ok" from the Mac; integrity check prints ok. The phone check (H-2) is a HUMAN step, reported done or outstanding, not part of the gate.
8. HUMAN steps
From SPEC §11.2 (v0.3), verbatim: "gcloud auth login; on the phone on the home Wi-Fi, open the dev server's /health and answer yes/no that it shows "status":"ok"". No open question blocks this plan (OQ-3 only blocks M2c-pi).
- H-1, before step 7, on the owner's Mac:
gcloud auth login(sign in with the owner's Google account in the browser that opens). Expected result: the command ends with a "You are now logged in as" line. - H-2, after step 8: on the owner's phone on the home Wi-Fi, open
http://<Mac LAN address>:<port>/healthin Chrome and answer yes/no: the page shows"status":"ok". If macOS asks whether Docker may accept incoming connections, the owner clicks Allow. - H-3 (only if step 2 finds a tool missing; not a §11.2 step): the owner decides whether and how to install it.
9. Out of scope
- Everything that concerns the Pi (M2c-pi):
infra/compose.yaml,infra/Caddyfile, cert-sync image,infra/build-images.sh,infra/deploy.sh,infra/publish-apk.sh,infra/README.md,infra/RESTORE.md, the Gitea registry and Gitea Actions, router forwarding, the Drive upload andrclone authorize, copying secrets to the Pi. - Server code (M2a, M3, M4); Android code and Gradle files (M2b and later).
- Changing org policies or billing in Google Cloud.
10. Report
- Branch
agent-m2c-dev, head commit hash after rebasing onmain. - Gate output lines:
configexit status, shellcheck result,/healthresponses (Mac, phone), integrity check output. - Versions chosen (images with digests, rclone) with sources; TLS option chosen.
- Firebase: app ids created (ids are not secrets), service account email, whether
google-services.jsonholds both clients; any policy block. - Where the secrets were written (paths only, never contents); the REST calls made (method and resource name only).
- Whether the dev stack is left running, and its URL.
- Proposed decision entries (never written to
SPEC.mdordocs/decisions.mdby this agent) orTODO(spec)occurrences. - HUMAN steps done and outstanding, worded for relay.
- Contract requests, open questions, spec gaps (with section numbers).
11. Numbering
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use TODO(spec) if none was given.