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

87 lines
12 KiB
Markdown

# 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.