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>
8.8 KiB
PLAN-M10: First release
1. Goal
Ship the first signed release to both phones from the production server on the Pi: release keystore, signed APK published, both phones paired, push working both ways, nightly backup on Drive, a restore drill, and the memory check. Record the evidence in docs/validation/first-release.md and docs/validation/restore-drill.md.
2. Read first
- SPEC §10.6 APK hosting and release, §10.9 Backups and restore, §10.10 Operations, §10.4 Server configuration and CLI (
bootstrap,rotate-epoch), §10.7 (step 7, the FCM key on the Pi). - SPEC §4 FR-1, FR-2 (pairing both phones), FR-13 Push notifications, FR-16 In-app update, FR-18 Backups and restore.
- SPEC §8.5 Transport (release network security config), §14.4.8 Epoch recovery.
- SPEC §11.2 (row M10), §11.3 (note M10), §14.7.
infra/README.md,infra/RESTORE.md,infra/publish-apk.sh,infra/deploy.sh(from M2c-pi);android/app/build.gradle.kts(signing config, from M8).CLAUDE.md(THE PHONE, THE OWNER: confirm before anything destructive; EXECUTION PROTOCOLS: Reports are deliverables).
3. Already established
version_code=git rev-list --count HEADof the released commit;version_name=YYYY.MM.DD-<short sha>; passed askvitt.versionCode/kvitt.versionNameby the release script (§10.6).- Release keystore
kvitt-release.jks, aliaskvitt, RSA 4096, validity 10000 days, created once by the owner with thekeytoolcommand of §10.6 in a directory outside the repository (D-024);android/local.propertiesholds its absolute path and the secrets inkvitt.release.storeFile,kvitt.release.storePassword,kvitt.release.keyAlias,kvitt.release.keyPassword; the owner keeps a copy in a password manager; losing it means reinstalling (§10.6). infra/publish-apk.shbuilds:app:assembleReleasethroughtools/gradle-serial.sh, writesapk/kvitt-<version_code>.apkand an atomicapk/latest.json, keeps the newest 5 (§10.6).- The landing page links "Download Kvitt" to the latest APK; the first install asks to allow "Install unknown apps" (AC-2.5, AC-16.3).
- Pairing:
kvitt-server bootstrapon the Pi prints the admin code (FR-1); the admin creates the partner invite in Settings and shares it (AC-2.1); after pairing the app syncs and asks for notifications (AC-2.9). - Push both ways: a change by one user notifies the other user's devices (AC-13.1).
- Backup nightly 03:30 to Drive folder
1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV; local files deleted once confirmed on Drive (D-002, §10.9). - Restore drill follows §10.9 steps 1 to 7 and is documented in
docs/validation/restore-drill.md; both phones must sync correctly afterwards (AC-18.2). - Memory:
docker stats --no-streamwith both phones syncing; server RSS under 100 MB (§10.10, §11.2). - Production runs on the Pi; development data on the Mac is not migrated (D-001).
4. Owned paths
From SPEC §14.7, verbatim: infra/** row, after-landing column "M10 updates runbooks" (that is infra/README.md, infra/RESTORE.md), and docs/validation/<file> | "the milestone that names the file", here docs/validation/first-release.md and docs/validation/restore-drill.md.
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
5. Deliverables
docs/validation/first-release.md: release version, publish result, pairing of both phones, push both ways, update check, backup on Drive, memory check result, outstanding items; a "Results (owner's phone)" section for anything still to come; aggregates only.docs/validation/restore-drill.md: each §10.9 restore step as run, timings, the epoch rotation, both phones' re-push and balance check, issues found.infra/README.md,infra/RESTORE.md: corrections found during the release and the drill.- On the Pi (not in git): published
apk/kvitt-<version_code>.apkandapk/latest.json; a production household with both users.
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m10 -b agent-m10 main, then in.claude/worktrees/agent-m10:git reset --hard main. Copyandroid/local.properties,android/app/google-services.jsonandinfra/secrets/from the main checkout; the keystore stays outside the repository and is referenced by absolute path fromlocal.properties(never copied into a checkout or git). Runtools/install-git-hooks.sh. - Read §2 in order. Confirm M2c-pi, M3, M4 and M8 have landed and the Pi stack is healthy (
infra/deploy.shoutput,/health); if not, stop and report. - Keystore (H-1). After the owner created it, add the four
kvitt.release.*keys to the main checkout'sandroid/local.properties(never overwrite other lines). - Deploy the current
mainimage to the Pi withinfra/deploy.sh <tag>; publish the APK withinfra/publish-apk.sh; checkGET /apk/latestreturns the newversion_code. - Bootstrap production:
docker compose exec server kvitt-server bootstrapon the Pi (over SSH); give the code to the owner through the orchestrator. - Owner's phone (H-2 to H-4): install from the landing page, pair, allow notifications. Confirm with
adb shell pm list packages | grep se.hagstedt.kvittthat the release id is installed and does not clash with the debug build. - Partner (H-5): the owner creates the partner invite in Settings and shares it; the partner installs, pairs, completes the profile step.
- Push both ways (H-6): one test expense from each phone (deleted afterwards); each phone confirms the notification.
- Backup: trigger
docker compose exec backup /backup.sh; confirm on Drive withrclone lsf; confirm the next nightly run the following day if the session allows, else leave it in "Results (owner's phone)". - Restore drill (confirm with the owner before step 1 of §10.9 because it stops production): follow
infra/RESTORE.mdexactly with the latest backup; both phones re-push after the new epoch; the agent compares balances from the server with what the phones hold (pulled over USB from the owner's phone; the partner phone by yes/no). Fix the runbook where it was wrong. - Memory check:
docker stats --no-streamon the Pi while both phones sync; record the server's memory. - Write the two validation files. Commit on
agent-m10,git rebase main. Never merge, push or remove the worktree.
7. Gate
From SPEC §11.2, verbatim: "docs/validation/first-release.md and docs/validation/restore-drill.md complete; server RSS under 100 MB".
Green means: both files exist with every section filled (no TODO other than a "Results (owner's phone)" item named as outstanding); the docker stats line recorded in first-release.md shows the server below 100 MB with both phones syncing.
8. HUMAN steps
From SPEC §11.2, verbatim: "Create the release keystore; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive".
- H-1, step 3, on the owner's Mac, in a directory outside the repository (D-024):
keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000; choose the passwords, store them in the password manager, and tell the agent the keystore's absolute path and where the passwords are entered (the agent never echoes them). - H-2, step 6, on the owner's phone: open the invite link or
https://icehack.asuscomm.com:8443/i/<code>, tap "Download Kvitt", open the download. - H-3, step 6: when Android asks, allow "Install unknown apps" for the browser, install, open Kvitt, pair with the code, allow notifications.
- H-4, step 6 and later updates: allow "Install unknown apps" for Kvitt itself when the in-app update first asks (AC-16.3).
- H-5, step 7, on the partner's phone: the same as H-2 and H-3 with the partner invite link, then the profile step.
- H-6, step 8: each phone answers yes/no: a notification about the other's test expense arrived.
9. Out of scope
- New features or fixes in app or server code (findings go in the report; fixes are new plans).
- The Splitwise import (M11).
- Gitea Actions changes and Pi provisioning beyond runbook corrections (M2c-pi).
- Deleting anything on Drive.
10. Report
- Branch
agent-m10, head commit hash after rebasing onmain. - Released
version_codeandversion_name; image tag deployed. - Gate: the two files' section list and the
docker statsserver line. - Restore drill result in one line; runbook corrections.
- Versions: none expected beyond what landed; state any tool used (keytool, rclone) and its version.
- 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.