Files
kvitt/PLAN-M2c-dev.md
jacobh2 76ee957f41 SPEC v0.3: orchestrator resolutions of the plans' open points
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>
2026-10-17 21:23:38 +02:00

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

  1. 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 server and backup images), §10.4 Server configuration and CLI, §10.7 Firebase setup, §10.9 Backups and restore (backup steps 1 to 5).
  2. SPEC §4 AC-2.7 and AC-2.8 (debug builds accept http:// to private LAN addresses), FR-20 Health.
  3. SPEC §8.7 Privacy and logging, §8.9 Server hardening (container user, read-only root, pragmas).
  4. 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.
  5. docs/decisions.md D-001, D-002, D-022.
  6. 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.yaml or plain cargo run; debug builds pair against it (D-001, §10 intro).
  • The dev stack has server, an optional caddy with a self-signed certificate or no TLS for local use, and backup runnable on demand without the Drive upload (D-001, §10 intro, §11.2).
  • backup.sh steps: .backup snapshot, PRAGMA integrity_check must print ok, gzip and pictures tar with a UTC stamp, rclone copy only, last-success marker; when RCLONE_FOLDER_ID is 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).
  • backup image: alpine with sqlite, gzip, tar, rclone, infra/backup/backup.sh run by crond (§10.3).
  • Server config is only the env variables of §10.4; KVITT_FCM_CREDENTIALS default /secrets/fcm-service-account.json, push disabled with a warning when missing; KVITT_FCM_PROJECT_ID kvitt-1337 (§10.4).
  • Firebase (D-022): no new CLI installs; gcloud exists, the Firebase CLI is not used; the Firebase project and the Android apps are created through the Firebase Management REST API with gcloud auth print-access-token; rclone belongs to M2c-pi. Project kvitt-1337; Android apps se.hagstedt.kvitt ("Kvitt") and se.hagstedt.kvitt.debug ("Kvitt debug"); one merged google-services.json in infra/secrets/ (git-ignored), which M8 copies into android/app/ with infra/dev/copy-google-services.sh; service account kvitt-fcm with roles/firebasecloudmessaging.admin, key at infra/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 from server/Dockerfile, data in a named volume, FCM key mounted read-only from infra/secrets/, KVITT_PUBLIC_URL set to the Mac's LAN URL, user 10001, read-only root, no capabilities, no-new-privileges), optional caddy, backup (on demand), logging and TZ=Europe/Stockholm as §10.2.
  • infra/dev/Caddyfile: only if the stack includes Caddy (self-signed via tls internal, or plain HTTP).
  • infra/dev/README.md: start, stop, logs, bootstrap and pair-device commands, 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_ID local mode.
  • infra/dev/.env.example: the variables infra/dev/compose.yaml reads, with dev-friendly values (no secrets); the gate runs config with it.
  • infra/env.example: the .env keys of §10.2 with comments (no secret values).
  • infra/dev/copy-google-services.sh: copies infra/secrets/google-services.json to android/app/google-services.json of the checkout it runs in (exits non-zero with a message when the source is missing); shellcheck clean.
  • Firebase project kvitt-1337 with both Android apps (cloud state, no repository file).
  • infra/secrets/fcm-service-account.json (git-ignored, main checkout).
  • infra/secrets/google-services.json with both clients (git-ignored, main checkout).

6. Steps

  1. 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 of infra/secrets/ (mkdir -p infra/secrets && cp -R /Users/jhag/projects/kvitt/infra/secrets/. infra/secrets/ if present). Run tools/install-git-hooks.sh.
  2. 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). If docker or gcloud is missing, stop and ask the owner (do not install).
  3. Versions: resolve from official sources and record: the alpine image tag and digest (Docker Hub official image), rclone package version in that Alpine release, the caddy image version and digest if Caddy is used (Docker Hub official image). Pin by version and digest.
  4. Write infra/backup/backup.sh and infra/backup/Dockerfile; run shellcheck on the script.
  5. Write infra/dev/compose.yaml (and infra/dev/Caddyfile if used), infra/env.example, infra/dev/README.md. Record which TLS option (self-signed or none) was chosen and why.
  6. Run docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config.
  7. 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 with TOKEN=$(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.addFirebase for projects/kvitt-1337 (skip if already a Firebase project), projects.androidApps.create for "Kvitt" / se.hagstedt.kvitt and "Kvitt debug" / se.hagstedt.kvitt.debug (skip an app that already exists), projects.androidApps.getConfig for each, merged into one /Users/jhag/projects/kvitt/infra/secrets/google-services.json holding both clients. Never print the config or the token. Then gcloud iam service-accounts create kvitt-fcm --display-name "Kvitt FCM sender"; grant roles/firebasecloudmessaging.admin on 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.
  8. 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>/health on the Mac; then the owner's phone check (§8 H-2), relayed by the orchestrator.
  9. Backup check: run the backup job once on demand (docker compose -f infra/dev/compose.yaml run --rm backup /backup.sh or as written in the README), then sqlite3 <snapshot> "PRAGMA integrity_check" prints ok.
  10. Run shellcheck $(find infra -name '*.sh').
  11. 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>/health in 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 and rclone 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 on main.
  • Gate output lines: config exit status, shellcheck result, /health responses (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.json holds 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.md or docs/decisions.md by this agent) or TODO(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.