d9e189abd5
Build Kvitt images / build (push) Successful in 5m45s
D-125 (owner decision 2026-10-20, landed6e5981d): the Kvitt Pi runs Kvitt from /home/jacobhagstedt/kvitt, a self-contained compose project with relative bind mounts, both containers as the owner's uid/gid, the seven .env keys compose reads, no scripts and nothing under /opt; operations are plain docker compose commands. Revises D-103, D-105, D-117 point 5 and D-120 point 6. SPEC §10.2, §10.6, §10.8 to §10.10, §11.2 and §14.7 rewritten; KVITT_PI_HOST placeholders become <kvitt-pi-address>. §10.3 records the Gitea workflow's first results (run 69: v1.0.0, backup 5 min 20 s, server 2 h 42 min; v1.0.1 from the sparse-checkout push) and the sparse checkout of3820f9c. HANDOFF, prompts/README.md and notes in PLAN-M11.md and PLAN-M2c-pi.md follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
26 KiB
26 KiB
Handoff: state of the work
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
1. Where things stand
mainat28fa71b(the compose deploy hint in the workflow andinfra/build-images.sh, after6e5981d, the self-contained Pi compose project, D-125);origin/main(Gitea) at6e5981d, so28fa71bis not pushed yet; plus the SPEC v0.24 docs commit fromagent-docs-v024bonce it lands. Landed since SPEC v0.23 (b1f4c10):3820f9c,200c99f,6e5981d,28fa71b(below).- Kvitt host name (owner decision 2026-10-19):
kvitt.suorra.duckdns.org, a subdomain of the owner's DuckDNS family domainsuorra.duckdns.org; NPM issues its Let's Encrypt certificate;KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org, soKVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org(port 443, D-124). It is ininfra/pi/.env.exampleandinfra/pi/.env(KVITT_PUBLIC_HOST) andandroid/local.properties(kvitt.publicHost, git-ignored). SPEC §10.1 and §10.5.1 point here. - NPM proxy host for
kvitt.suorra.duckdns.orgis Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124). - Images:
gitea.lan/jacobh2/kvitt-serverandgitea.lan/jacobh2/kvitt-backup, tagsv1.0.0(run 69) andv1.0.1(the sparse-checkout push; the tag the Pi uses), pluslatest; the packages are public, so the Pi pulls withoutdocker login. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok,v1.0.0,kvitt-backup5 min 20 s,kvitt-server2 h 42 min on the Pi 3 (first build,CARGO_BUILD_JOBS=1; later runs reuse the builder's cargo cache); the tag push works with thewrite:packagetoken. SPEC §10.3. - Kvitt Pi (D-125): Kvitt runs from
/home/jacobhagstedt/kvittas the owner's userjacobhagstedt, uid and gid 1000:1000 (KVITT_UID/KVITT_GID); Docker and thegitea.lanregistry trust are already on the Pi; nothing outside that folder is installed or changed. The owner is installing now (2026-10-20, §3 item 1). - History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in
18c527f; pack 45 MiB;origin/mainpushed at18c527f. A bundle of the pre-rewrite history (kvitt-pre-rewrite.bundle) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (7c453d8before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC,docs/decisions.md, this file andprompts/README.mdcite the rewritten hashes since v0.18 (7c453d8stays as the oxipng pass's name;docs/validation/ac-coverage.mdkeeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes inPLAN-*.mdreplaced by the commit with the same subject);docs/spikes/pi-image-arm64.mdand the READMEs had none stale. For any other old hash, find the commit by its subject withgit log --oneline --grep. - History (rewritten hashes):
5e1be38plans and prompts,c885abaM2c-dev,76ee957SPEC v0.3,c277ee3M2b,06f48efM2a,6dce4c7SPEC v0.4,7869ec4M3,8f0e156M5,853c038SPEC v0.5,15056dcM4,d9dd584SPEC v0.6,50339aeM9,86e08a1SPEC v0.7,aa765bbM12a,d39eb2cSPEC v0.8,b09f322M12c,6eaccecM12b,0006bd7UX Kalas categories and tokens,6da6b1bUX sprint U2,e676812SPEC v0.9,557af7eM12d,7730a31M6,9ee67d4SPEC v0.10,a41f240and6453932app wiring before M8,1ea752cM7a,ea7d494M7e,7380e50M7d,0e1d99dM7b,0b140aaM7c,c4ab35fM7f,b24c8b5M6b,f4dc10cSPEC v0.11,3dda414M7g,cb832a7and3d9eea8M8,6159e18SPEC v0.12,8c60715M12e,b7b7483M12e launcher icon,51d6ff8SPEC v0.13,a312758Vico removal,f56dcbeFCMfid,3e6c337Pi image arm64 check,e2dba84SPEC v0.14,328c466M8b,6eb3f85FcmMessageBody.fid,bfde12cSPEC v0.15,75b549fM8c goldens,a0c25b0M8c licences row,c30a49eR8 release build,bacbd18M8d,bfc3acbSPEC v0.16,2476667M2c-pi bundle,a59abebM8e,b4d9467DockerfileCARGO_BUILD_JOBS,956908aM8f,1328afechips cleanup,400eac9SPEC v0.17,18c527fgoldens restored after the rewrite; then2fac1ectoe70fe0dinfra overhaul,8660b7fserver gap pass,7fb86f5infra security,dc7a296toa0259abM12f,8103daetob985ce9server overhaul,48fe119SPEC v0.18,e4e6e5dtoc582363Android overhaul,3ef2c15SPEC v0.19,96e4cbdAPK download rate limit (D-119),481889aSPEC v0.20,f1abb0dcargo auditin the server gate,4f69ae0Gitea login in the run line,d05f13aPi stack behind NPM,62f5279and9231a1dinfra README for NPM (D-120),d090c6cAndroid host property (D-121),4bcff2cSPEC v0.21,dd07ca4temporary workflow diagnostic step,4a4e45fS-22's release server and4b1ae0cpublish-apk.shhost (D-122),c9abd24ande381e37server host defaults and OpenAPI{host}(D-123),ac17b41install.shstep number,cce3e4eSPEC v0.22,a86ad24server and contracts on port 443,4852803bundle on port 443 (D-124),038c603Android on port 443 and the pinned-root template,ad35231portless Android fixtures,b1f4c10SPEC v0.23,3820f9csparse-checkout workflow,200c99f.envwithout informational keys,6e5981dself-contained Pi compose project (D-125),28fa71bcompose deploy hint in the workflow andinfra/build-images.sh. - Landed since SPEC v0.23 (recorded in v0.24):
- Sparse-checkout workflow (
3820f9c, no decision): cone-modesparse-checkoutofserverandinfra/backup(the two build contexts), so the action fetches with--filter=blob:noneand no PNG is checked out;fetch-depth: 0stays becausefetch-tagswith a shallow fetch only brings tags on the tip; the temporary "Diagnose secrets" step removed. Its push builtv1.0.1. .envwithout the informational keys (200c99f, no decision; revises D-103):KVITT_PI_HOSTandKVITT_PI_SSH_USERdropped, read by nothing on the Pi; the README'sscp,sshand NPM steps name<kvitt-pi-address>.- Self-contained Pi compose project (
6e5981d, D-125, owner decision 2026-10-20: 'It should be a self contained docker compose thing', 'do not install anything in any other place'):/home/jacobhagstedt/kvittholdscompose.yaml,.env,secrets/and the bind-mounteddata/,backups/,import/; both containers asKVITT_UID/KVITT_GID(1000 on the Pi), read-only root, 16 MB tmpfs/tmp, each mounting only its own secret file (create_host_path: false);.envexactly the seven keys compose reads,RCLONE_REMOTEfixed togdrive:;install.sh,deploy.sh,bootstrap.sh,backup-now.sh,install-apk.sh,lib.shremoved, operations are plaindocker composecommands (infra/README.md"Pi: what the owner does", seven steps, andinfra/RESTORE.md); no digest record (docker image inspectinstead); APKs byscpintodata/apk/. Rehearsed with Podman as uid 501: healthy,/health200, bootstrap, APK served, backup in three modes, restore steps. - Compose deploy hint (
28fa71b, no decision): the workflow's header and digest-step comment andinfra/build-images.sh's closing line namecd ~/kvitt,KVITT_TAGin.env,docker compose pull && docker compose up -danddocker image inspectinstead of/opt/kvitt/deploy.shand/opt/kvitt/deployed-digests.
- Sparse-checkout workflow (
- Landed since SPEC v0.22 (recorded in v0.23):
- Port 443 (D-124, owner decision 2026-10-19): Kvitt is served at
https://<host>with no explicit port, because NPM listens on 443 and selects by host name; the router's existing external-443 forward to the Home Assistant Pi serves Kvitt, no 8443 rule. Server and contracts (a86ad24): OpenAPI serverhttps://{host}, E-7 and E-12 examples without a port,tools/contracts-check.pyfollows. Bundle (4852803):KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}, health checks through NPM on 443, README step 7 "Router: no new rule", the APK-abuse stop-gap is disabling Kvitt's proxy host in NPM. Android (038c603):releaseServerUrl(host)=https://<host>(RELEASE_SERVER_PORTremoved), the AC-2.6 intent filter without a port (matches every port, so an old:8443link still opens S-1 with the server it names), invite-link parsing drops:443and:80, 28 S-1 goldens onhttps://kvitt.example.org. - Portless Android fixtures (
ad35231, D-124):android/README.md, theAppBuildInfoandAboutRepositoryImplcomments, the S-21 and S-22 fake scenarios and the:datatest fixtures without:8443; 30 goldens re-recorded (6 S-21 invite-created, 24 S-22). What is left of8443inandroid/is deliberate: tests that an old:8443link still opens S-1 and that a typed port survives. - Pinned-root template (
038c603, no decision): withkvitt.tls.pinnedRoot=truethe build generates the pinned-root network security config fromandroid/app/src/releasePinnedRoot/network_security_config.xmlwith the domainkvitt.publicHostand stops with a message whileandroid/app/src/release/res/raw/kvitt_root_ca.crtis not committed (SPEC §8.5, §10.5.3). The old host name is out of that config and of the S-1 fixture.
- Port 443 (D-124, owner decision 2026-10-19): Kvitt is served at
- Landed since SPEC v0.21 (recorded in v0.22):
- D-121 follow-ups (
4a4e45f,4b1ae0c; D-122):AppBuildInfo.releaseServerUrlfromRELEASE_SERVER_URL; S-22 shows the paired server, before pairing the build's release server;DataModule.DEFAULT_SERVER_URLremoved; fakes and tests onhttps://kvitt.example.org:8443(30 S-21/S-22 goldens re-recorded);infra/publish-apk.shreadsKVITT_PUBLIC_HOSTfrominfra/pi/.env(refuses missing,CHANGE_ME, a URL, the old name) and passes-Pkvitt.publicHost. - Server host defaults (
c9abd24,e381e37; D-123):KVITT_PUBLIC_URLrequired (every command exits 1 without it, before opening the database);KVITT_TRUSTED_PROXYdefaults tonone; OpenAPI serverhttps://{host}:8443(portless since D-124) with variablehost(defaultkvitt.example.invalid); E-7 and E-12 examples onkvitt.example.invalid;tools/contracts-check.pyfollows. 276 server tests. install.shnames the NPM proxy host as README step 6 (ac17b41, no decision).- Temporary "Diagnose secrets" step in
.gitea/workflows/build-images.yaml(dd07ca4, no decision), to be removed once the login failure's cause is known (removed in3820f9c).
- D-121 follow-ups (
- Landed since SPEC v0.20 (recorded in v0.21):
cargo auditin the server gate (f1abb0d, no decision): the gate iscargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test; the pre-commit hook runscargo audit --no-fetchonly whenserver/Cargo.lockor a serverCargo.tomlis staged. First run: 0 advisories over 319 crates.- NPM rework (
4f69ae0,d05f13a,62f5279,9231a1d; D-120): TLS terminates in the owner's Nginx Proxy Manager on the Home Assistant Pi; Caddy, cert-sync, the Caddyfile, the HA SSH key and the router forward to the Kvitt Pi are gone. Kvitt gets its own host nameKVITT_PUBLIC_HOST(owner-chosen; a DuckDNS name is being set up), becauseicehack.asuscomm.comis NPM's proxy host for Home Assistant. Router external 8443 → HA Pi 443 (replaced by the existing 443 forward, D-124). The server publishes 8080 on the Kvitt Pi's LAN,KVITT_TRUSTED_PROXY=${HA_HOST}(NPM's IPv4, checked byinstall.sh). SPEC §10.5 rewritten (NPM proxy host, the Custom Nginx Configuration with D-117's headers and the/i/log exclusion, fallback: no LAN path for release builds). Rehearsed on the Mac:nginx -tagainst NPM v2.16.0 templates, 21 checks of the server behind nginx passed. The Gitea login interpolates the secret in therunline (the env form came through empty on act_runner 0.6.1). - Android host property (
d090c6c, D-121):kvitt.publicHost(Gradle property, elseandroid/local.properties, elsekvitt.example.invalid),BuildConfig.PUBLIC_HOST, the manifest placeholderpublicHost,RELEASE_SERVER_URL,@ReleaseServerUrlin:feature:onboarding. 944 Android tests.
- Landed since SPEC v0.19 (recorded in v0.20):
- APK download rate limit (
96e4cbd, D-119, security review L-3): E-6GET /apk/files/{file_name}at most 10 requests per minute per client IP in its own window, not counted against the 60 of the other unauthenticated endpoints; a 404 counts; 429rate_limitedwithRetry-After; E-5 and E-4 stay on the 60; unit and integration tests.
- APK download rate limit (
- Landed since SPEC v0.18 (recorded in v0.19):
- Android overhaul (
e4e6e5d,8229ed6,515b60f,e08aaf0,0714242,a92c236,74b5fbd,c582363; D-118): shared test support in:core:design's test fixtures (se.hagstedt.kvitt.design.testing),kvitt.android.screenshotswires Roborazzi and the test libraries;format/Failures.kt,format/Limits.kt,savedSplitLabelin:core:design; dead code removed (KvittMotiononly the seesaw spring,KvittIcon.Description, unused dimens and shapes); four catalog entries removed; oneCopyCategoryKeywordstask; oneconfigureAndroidModule; S-7's state types inExpenseEditorState.kt. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Android overhaul (
- Landed since SPEC v0.17 (recorded in v0.18):
- Infra overhaul (
2fac1ec,61f92c8,fc6126c,b647faf,e70fe0d; no decision):tools/gradle-lock.shshared bygradle-serial.sh,clean-builds.shandinfra/build-images.sh; the pre-commit hook runs throughgradle-serial.shwith JDK 25 onPATH(hooks reinstalled inmainaftere70fe0d);install.shkeeps the deployedKVITT_TAG;--provenance=falseon buildx pushes, the workflow token through env;RCLONE_REMOTEdropped from the dev env. - Server gap pass (
8660b7f, D-113):kvitt-server invite-partner(0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device;push sentinfo line; CLI tests (invite-partner,migrate,healthcheck, SIGTERM drain);tools/contracts-check.py134 checks. - Infra security (
7fb86f5, D-117): Caddy log redaction of/i/<code>and query strings, CSP and three more response headers, Caddy non-root (uid 10002, read-only, caps dropped),/certs0750, dev stack mounts only the FCM key,/opt/kvitt/deployed-digestswith a WARNING on change,kvitt_cicdwithwrite:packageonly, the plain root key (old variant B) not acceptable. - M12f (
dc7a296,5b2ed37,2bd6b58,c366fbd,a0259ab; D-109 to D-112, D-116): S-16 lost version read-only, S-22 " (schema )" from/health,SettlementRepositoryreturns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen,localeFiltersen/sv,:datatests (429, D-040, WorkManager viawork-testing), the home/account screenshot harness fix (stalesv_darkgoldens re-recorded), 19 unused string keys removed (strings.csv562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Infra overhaul (
- Validation reports (SPEC §11.4):
docs/validation/ac-coverage.md(taken at956908a,ed1b130before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f.docs/validation/security-review.md(taken at18c527f): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (e70fe0d); H-1 (725901c, D-115), M-3 (baa5806) and L-4 (b985ce9, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps thedrivescope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (96e4cbd, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (725901c). - Server overhaul (landed on
main8103daetob985ce9): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (725901c, D-115), pictures at most 16 MP and 64 MB decoded (baa5806, D-114),fcm_tokenat most 512 characters (b985ce9, D-114); shared row inserts, invite creation and CLI helpers,tests/common/harness, synchronoustasks::start; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18. - In flight:
- This docs pass (
agent-docs-v024b: SPEC v0.24, D-125, HANDOFF,prompts/README.md, notes inPLAN-M11.mdandPLAN-M2c-pi.md). - The owner installing Kvitt on the Kvitt Pi (§3 item 1).
- This docs pass (
- Earlier outcomes (details in SPEC §11.2, §11.3 and
docs/decisions.md): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push byfiddelivered); M8b to M8f UI fixes; the M2c-pi bundleinfra/pi/(D-103 to D-106, owner checklist ininfra/README.md"Pi: what the owner does"); release builds R8-shrunk and signed (aliaskvitt, RSA 4096, v2). - Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
- Remote:
originon Gitea, SSH key works; pushed at6e5981d. The Gitea secretkvitt_cicd(a user-level secret ofjacobh2, renamed by the owner 2026-10-19) works; the Drive token isinfra/secrets/rclone.conf([gdrive]; folder id in.envasRCLONE_FOLDER_ID). Unused on the Mac since D-120:infra/secrets/ha_ssh_keyandha_ssh_key.pub(git-ignored; may be deleted). - Both gates exist: Android
tools/gradle-serial.sh check(fromandroid/), servercargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test(fromserver/,cargo auditsincef1abb0d). Both green onmainas far as reported (Android 944 tests atd090c6c, one more test in4a4e45f; server 276 tests atc9abd24). - Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The CSV files stay in
export/(git-ignored). - Development stack (M2c-dev): running on the Mac at
http://192.168.1.210:8080, database at schema 3, data kept; backupsinfra/dev/backups/kvitt-20261018T093143Z.sqlite.gzandkvitt-20261018T130443Z.sqlite.gz(git-ignored). On this Podman setup runinfra/dev/up.sh --down, theninfra/dev/up.sh, to swap in a rebuilt image. Since7fb86f5up.shcopies onlyfcm-service-account.jsonintoinfra/dev/secrets/(git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase projectkvitt-1337with appsse.hagstedt.kvittandse.hagstedt.kvitt.debug; service accountkvitt-fcm. - Installed on the owner's phone (SM-S942B): the debug app
se.hagstedt.kvitt.debug, paired against the dev server; the release appse.hagstedt.kvitt, rebuilt with the hostkvitt.suorra.duckdns.organd no port (D-124) and installed 2026-10-19; release pairing waits for the Kvitt Pi (release builds requirehttps, AC-2.8; §3 item 3). - Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run
tools/clean-builds.shwhen it drops under about 5 GB.
2. Open work (one line each, with who owns it: agent or owner)
- Land
agent-docs-v024b(orchestrator): SPEC v0.24, D-125, HANDOFF,prompts/README.md, the plan notes. - Closed by
28fa71b: the/opt/kvittleftovers in.gitea/workflows/build-images.yamlandinfra/build-images.sh(compose deploy hint,docker image inspect). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min). - Push
28fa71band the v0.24 docs commit toorigin(orchestrator).28fa71btouches.gitea/workflows/**, so the push starts a workflow run (next tagv1.0.2, same image content; the server build should reuse the builder's cargo cache). infra/README.md"Gitea Actions workflow" is stale in two places (agent, next infra pass):CARGO_BUILD_JOBS"takes effect only onceserver/Dockerfiledeclares it" (it does sinceb4d9467) and "the first build's duration on the Pi is unknown"; its "Owner decisions (open)" 1 (write:repository, moot: the tag push works) and 2 (host name, answered) are answered.infra/backup/Dockerfile: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).- Historical
:8443andicehack.asuscomm.commentions indocs/ux/anddocs/validation/are left as the record of their time (no action unless the owner wants them rewritten). - Restore drill AC-18.2 (
docs/validation/restore-drill.md, M10; the one Missing row of the AC-coverage audit);infra/RESTORE.mdis the runbook since D-125. - Possible firewall rule on the Kvitt Pi limiting port 8080 to the HA Pi (
HA_HOST); not done, not decided (owner). D-125 makes no system changes, so it would be the owner's own step. - AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- A
KvittWorkerFactory.createWorkertest (Android agent, no plan yet). - Security review L-6: an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1; owner, §4).
- Security review L-10:
forbiddenornot_foundfor the other user's device id (owner decision, §4). - State-class naming:
*Ui,*UiStateand*Stateare mixed across features; needs a decision if a convention is wanted (owner; no plan). - Unverified risk: the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the next runs).
PLAN-M2c-pi.mdis superseded where it names Caddy, cert-sync, scripts or/opt/kvitt(note at its top, D-120 and D-125); SPEC §10 andinfra/README.mdare current.PLAN-M11.mdhas a note that/opt/kvitt/importis now~/kvitt/import.- Plans (SPEC §11.2 v0.24), in dependency order; prompt
prompts/<id>.md:- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit,
cargo audit, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124): landed. - Next: the owner installs Kvitt on the Kvitt Pi (§3; image
v1.0.1, which carries the H-1 fix like every image since725901c); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure SPEC §10.8 "Production run", now in~/kvitt/import/, D-025, D-125; each run must reconcile, D-045).
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit,
3. HUMAN steps pending (relay in chat)
- Pi install (D-125;
infra/README.md"Pi: what the owner does", steps 1 to 3). On the Mac:cp infra/pi/.env.example infra/pi/.env, setHA_HOST(NPM's LAN IPv4 address) andKVITT_TAG=v1.0.1, checkKVITT_UID/KVITT_GIDare 1000;mkdir -p infra/pi/secrets && cp infra/secrets/fcm-service-account.json infra/secrets/rclone.conf infra/pi/secrets/;scp -r infra/pi/compose.yaml infra/pi/.env infra/pi/secrets jacobhagstedt@<kvitt-pi-address>:kvitt/;rm -r infra/pi/secrets. On the Pi:cd ~/kvitt,chmod 700 secrets && chmod 600 secrets/*,mkdir -p data/apk backups import,docker compose pull && docker compose up -d,docker compose ps,curl -s localhost:8080/health. Expected:serverandbackupboth(healthy)within a minute;curlprints{"status":"ok",...,"db_ok":true}(yes/no). - From outside: on the phone with Wi-Fi off, open
https://kvitt.suorra.duckdns.org/healthin Chrome. Expected:"status":"ok"without a certificate warning (yes/no). - First backup, bootstrap, release pairing. On the Pi in
~/kvitt:docker compose exec backup /backup.shends withbackup: ok <stamp>(yes/no); thendocker compose exec server kvitt-server bootstrapprints the admin invite code, link and expiry; pair the release appse.hagstedt.kvittwith it (the orchestrator relays the exact screen labels then). - Partner phone, after the owner's release app is paired:
docker compose exec server kvitt-server invite-partneron the Pi; the partner installs the release APK and pairs with that invite (relayed with screen labels when the time comes).
4. Owner questions (asked, unanswered)
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
- Security review L-10: should a member get
not_foundinstead offorbiddenfor a device id of the other user (hides whose it is)? As built and as the spec says:forbidden. - State-class naming (
*Ui,*UiState,*Statemixed): fix one convention, or leave as built? - Answered 2026-10-20: Kvitt on the Pi is a self-contained compose project in
~/kvittwith nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name iskvitt.suorra.duckdns.org(§1); Kvitt on port 443 through the existing router forward (D-124). Moot:write:repositoryfor the registry secret (the tag push works withwrite:package).
5. Paused (waiting on something outside the repo)
- Nothing.
6. Numbering
- Decisions: D-001 to D-125 used, except D-008 and D-009 (never used); nothing reserved. Next free: D-126.
- Spec version: v0.24 written; next v0.25.
- Server schema version 3 (migration 0003); next 4. Room schema version 1 (
android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json); next 2. Both unchanged by v0.24. - Sync protocol 1, push payload 1.
7. How the work is run
See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not read them in files.