KVITT_PUBLIC_URL is required: every command refuses to start without it (compose
always sets it). KVITT_TRUSTED_PROXY defaults to none, which the rate limiter never
resolves. contracts/openapi.yaml and the utoipa document: servers[].url
https://{host}:8443 with variable host (default kvitt.example.invalid). E-7 and E-12
examples on kvitt.example.invalid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Reads KVITT_PUBLIC_HOST from infra/pi/.env without sourcing it, refuses a missing file or key, a CHANGE_ME value,
anything that is not a host name, and icehack.asuscomm.com (the checks of infra/pi/install.sh), passes it to
:app:assembleRelease as -Pkvitt.publicHost and prints the release server in the header and summary. README
publish paragraph updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AppBuildInfo gains releaseServerUrl, which :app fills from RELEASE_SERVER_URL (https://<kvitt.publicHost>:8443,
D-121). AboutRepositoryImpl shows it while no server is paired, in place of DataModule.DEFAULT_SERVER_URL, which
is deleted: :data no longer names a host. The fakes' AboutState uses the fixture https://kvitt.example.org:8443;
30 S-21/S-22 goldens re-recorded (only the host changed; oxipng -o 4 --strip safe). New test: unpaired About shows
the release server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Prints the lengths of secrets.kvitt_cicd, secrets.KVITT_CICD, the control
secrets.babymonitor_cicd, secrets.GITEA_TOKEN and github.token, plus the
repository, owner, actor, event, ref and server URL, before the registry
login. Values are never printed. Remove once the empty-secret cause is known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
SPEC §10.5 rewritten (10.5.1 Nginx Proxy Manager, 10.5.2 Custom Nginx Configuration, 10.5.3 Fallback:
NPM down); Caddy and cert-sync text replaced in §1, §6.1, §7, §8.5, §8.7, §8.9, §10 intro, §10.1 to §10.4,
§10.7, §10.10, §11.2 to §11.4, OQ-3, OQ-6, E-7 and §14.7; the Kvitt host name is KVITT_PUBLIC_HOST.
D-121: kvitt.publicHost, RELEASE_SERVER_URL and @ReleaseServerUrl (AC-2.6, AC-2.7, §10.6, §13.3 S-1).
Decisions D-120 and D-121 with pointers in D-023, D-103 to D-106 and D-117; HANDOFF; prompts/README;
a superseded-in-part note on PLAN-M2c-pi; dated D-120 notes under M-2, L-1, L-2, L-8 and I-5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The release server's host name comes from -Pkvitt.publicHost, else kvitt.publicHost in
android/local.properties, else the placeholder kvitt.example.invalid. :app exposes it as
BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost (the https invite-link intent
filter, port 8443) and RELEASE_SERVER_URL, which :feature:onboarding receives through the
@ReleaseServerUrl Hilt qualifier for S-1's prefill. Tests and goldens use fixed hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Owner checklist without the SSH add-on step: KVITT_PUBLIC_HOST placeholder, NPM's Details, SSL and
Advanced tab fields, the Custom Nginx Configuration block (security headers of D-117 at server level, its
own location / so NPM's X-Served-By does not drop them, /i/ kept out of NPM's access and error logs), what
NPM logs, the HSTS caveat, the router rule external 8443 to the Home Assistant Pi's 443, the NPM-down
fallback (no LAN path for release builds), operations and troubleshooting. RESTORE checks port 8080.
Rehearsal of 2026-10-19 recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The server publishes 8080 on the Kvitt Pi's LAN interfaces and trusts X-Forwarded-For only from HA_HOST,
NPM's LAN address (an IPv4 literal, checked by install.sh). KVITT_PUBLIC_HOST (owner-chosen, not
icehack.asuscomm.com, which is NPM's proxy host for Home Assistant) builds KVITT_PUBLIC_URL. Only
fcm-service-account.json and rclone.conf are installed; install.sh removes the Caddyfile, the Home Assistant
SSH key, known_hosts and the certs and caddy_data volumes left by the earlier stack. Health checks: port 8080,
then through NPM. The cert-sync image leaves the workflow and build-images.sh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The server gate becomes cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test
(CLAUDE.md, server/README.md). The pre-commit hook runs cargo audit --no-fetch only when server/Cargo.lock or a
server Cargo.toml is staged, and skips with a message when cargo-audit or the advisory database is missing.
First run: 0 advisories, 0 warnings over 319 crates; no audit.toml needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-119 (landed 96e4cbd, security review L-3): E-6 at most 10 requests per
minute per client IP in its own window, not counted against the 60 of the
other unauthenticated endpoints; SPEC §8.6, §10.6, E-6. M-1 closed by the
owner as an accepted risk (§8.1, §8.7); the D-114 limits confirmed (§11.4).
HANDOFF: main and origin at 96e4cbd, NPM rework (D-120) in flight, owner
steps and questions rewritten, numbering next D-121 and v0.21.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
E-6 (/apk/files/{file_name}) gets its own per-IP bucket of 10 requests per
minute (security review L-3), keyed like the 60/min unauthenticated limit
(peer or trusted-proxy X-Forwarded-For, D-032) and answered 429
rate_limited with Retry-After. E-6 no longer counts against the 60; E-5 and
E-4 stay on the 60/min limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AndroidApplicationPlugin and AndroidLibraryPlugin repeated the SDK levels, Java 17, unit-test resources, lint, Kotlin,
Robolectric JVM and JUnit set-up; both call configureAndroidModule(CommonExtension) now (the gate's task inputs did
not change: every test task stayed up to date). :app imports nothing from coil-compose; it still arrives through
:core:design, so the runtime classpaths are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
* Milestone and ownership notes that no longer hold ("owned by M5/M6/M8", "M8 replaces it", "(M8b)", "M7x add",
"proposed decision in the M5 report", which is D-027 and D-028) in build scripts, :core:design, :core:fakes, :data,
:app, :core:model and the settings string files.
* SyncEngine: the clock_skew KDoc sat orphaned above restampAfterClockSkew's; it now documents renewMutationId.
* libs.versions.toml header: no milestone, the drift date.
* README: the module table as built (no owners, no DebugFakesModule, which M8 removed), the test fixtures in the
architecture check and the shared screenshot harness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
DESCRIPTION_MAX (S-7 and S-19 each had 100), NOTES_MAX (S-7's 1000 and NotesField's private copy), FIRST_DATE and
DAYS_AHEAD_MAX (the S-7 and S-9 date pickers each spelled 2000-01-01 and 365). Values unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Nothing referenced any of these:
* :core:design KvittDimens iconMeta, seesawWidth, KEYPAD_COLUMNS, presetMinHeight, navIndicatorWidth/Height;
KvittShapes largeIncreased, extraLargeIncreased, extraExtraLarge (the FAB's 20 is `fab`); KvittSpacing
navigationBarHeight (ShortNavigationBar draws its own 64 dp); KvittMotion's spatial/effects springs, hero count,
bunting stagger, shared-axis, sheet-enter and keypad-swap values (only the seesaw spring is used); the
segmentedItem builder and snackbarMillis (KvittSnackbarVisuals maps the duration itself); KvittIcon.Description
with its two drawables.
* Catalogue sample Samples.renamedGroceries; fake scenarios SettleScenarios.partnerOwes and longAccountIds.
* :data Clock.todayDate().
* Unused imports in Common.kt, Settle.kt, Timeline.kt, AnalyticsScenarios.kt, Clock.kt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
* format/Failures.kt: FAILURE_NETWORK and commonFailureText, the §14.3.2 code-to-text map that :feature:account
(Errors.kt) and :feature:settings (Messages.kt) each spelled out; both keep only their own codes. The "network"
literals in :feature:onboarding and the two NETWORK constants use FAILURE_NETWORK.
* component/Split.kt: savedSplitLabel, the S-8 and S-16 line ("Kontots standard 50/50", "Egen fördelning 70/30",
"Exakta belopp", "Allt på dig/Anna") that ExpenseDetailScreen.modeLabel and SyncStatusScreen.lostModeLabel
duplicated line for line.
* format/Limits.kt: BP_TOTAL (five copies in four features and a literal in :core:design and S-19) and
DISPLAY_NAME_MAX (three copies in :feature:onboarding and :feature:settings).
Behaviour and goldens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Eight modules each had a copy of the same configuration-context harness (ScreenSnapshots/Snapshots); M12f had to fix
the same capture bug in two of them. :core:design's test fixtures (src/testFixtures, package
se.hagstedt.kvitt.design.testing) now hold Variant, TestClock, TestEnvironment, ScreenSnapshots with its three
capture modes (Root; Screen, with the hand-advanced frame clock; ScreenWhenIdle), MainDispatcherRule and withMain.
Test fixtures rather than a :core:testing module: :core:design's own tests can use them without a dependency on
itself, no module is added to the §6.2 graph, and lint treats them as test sources. The architecture check rejects
test fixtures outside test configurations. Modules keep only their own frame (analytics household, settle size and
inline overlays, settings avatar pictures). Every golden is unchanged.
kvitt.android.screenshots applies Roborazzi with outputDir src/test/screenshots and adds the screenshot test
libraries; the feature plugin applies it and adds the test fixtures, so the seven feature build scripts lose their
copied blocks. Removed: roborazzi-junit-rule and roborazzi-compose (never imported), the explicit
lifecycle-viewmodel-compose (feature plugin, :app) and hilt-lifecycle-viewmodel-compose in :app (both still arrive
through the features; release and debug runtime classpaths are identical), and the unused catalog entries
androidx-hilt-navigation-compose and androidx-lifecycle-viewmodel-compose. @Before/@After setMain pairs in nine test
classes use MainDispatcherRule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The task class and its wiring were duplicated verbatim in both build scripts; bundleCategoryKeywords(resourcePath)
registers it once per module. Resource paths and inputs are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- D-109 S-16 lost version read-only (Rejection.lost: RejectedVersion), D-110 S-22 versions from /health
(AboutState.schemaVersion), D-111 SettlementRepository nullable start/recompute, D-112 E-17 retry,
D-113 invite-partner and the push sent line, D-114 picture 16 MP / 64 MB and fcm_token 512 limits,
D-115 rate-limit token acceptance (revises D-032), D-116 private notifications (AC-13.9) and L-6 as
built (OQ-6), D-117 infra security (Caddy log redaction and headers, non-root Caddy, dev secret,
digest record, key form).
- §10.5.2 quotes infra/pi/Caddyfile at 7fb86f5; §10.4 invite-partner row; §11.2 M12f row; §11.3 server
gap pass, server overhaul, infra notes; new §11.4 validation reports; §14.7 server tree; strings.csv
562 rows; localeFilters, work-testing, harness fix, APK 10.0 MB.
- docs/validation/ac-coverage.md and security-review.md committed verbatim from the reports.
- Commit hashes in SPEC, decisions, HANDOFF and prompts/README remapped to the rewritten history (18c527f).
- design-spec: S-16 lost-version dialog, S-22 "(schema <n>)", the S-1 rationale mention dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
An authenticated device could store a token of up to the 1 MiB body limit, sent to FCM in every push for it. Now
(proposed D-114) more than 512 characters after trimming is 400 invalid_request with field fcm_token, like the
empty string (D-034); E-17's only documented error stays the only one. Test at 512 and 513.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AC-4.4 allowed 8000 x 8000 pixels: a few kilobytes of flat PNG or WebP decode to 256 MB RGBA, more than the server
container's 160 MiB. Now (proposed D-114) the header must also give at most 16 000 000 pixels and at most 64 MB
decoded (16 MP x 4 bytes; a 16-bit image is refused above 8 MP), checked before decoding, with the image crate's
decoder limits set as well; refused with the existing image_invalid. The decoded image is dropped before the square
is converted, and the square converts in place (into_rgba8/into_rgb8), so the peak is one decoded image plus one
square. Integration test at and over the pixel cap; unit test of the size rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Before, any response other than 401 (426 without X-Kvitt-Protocol, 405, ...) marked a random bearer token accepted
for good: it never counted against its IP and was never pruned, so a stranger could bypass the 60/min IP limit and
grow the in-memory map without bound. Now (proposed D-115, SPEC §8.6): a request with a token that is not accepted
counts against its IP before it runs and is refused once the IP is over; a 2xx accepts the token and gives that
count back (a newly paired device costs its IP nothing, as before); a 401 for an accepted token counts against the
IP and withdraws the acceptance; token entries are pruned once their window expires, accepted or not. Every 401
still counts against the IP exactly once. Unit tests for each rule and an integration test for random tokens
without the protocol header (60 x 426, then 429, the paired device unaffected).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
process<K> (158 lines) hands step 7 against a stored row to delete_stored<K>; same SQL, audit and push. kvitt-server
never reads a clap argument from the environment (Config reads it), so clap's env feature goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Taken from a -W clippy::pedantic scan, no allow added: map_or/map_or_else, method references instead of closures,
clone_from, write! instead of push_str(format!), borrowed Option<&str>, push_of without a needless Option, the
envelope's entity moved instead of cloned, the scheduler's plan() no longer a needless async fn (its doc now says
what it does on failure), sort_unstable, digit separators and inline format args in tests. Not taken: missing
# Errors/# Panics docs, must_use, doc backticks, assert!(x.is_empty()) in tests, too_many_lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
core_api/common.rs becomes tests/common/mod.rs and sync_api/m4.rs tests/common/sync_fixtures.rs; each binary
includes them through #[path] (the binaries of SPEC §14.7 stay as they are). TempDir's Default moves into the
harness instead of three copies; the skeleton helpers take ids, tokens and TempDir from it. Milestone wording in
test docs and the server README replaced by what the code does; README seams and test layout updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
crate::lock replaces three poison-tolerant lock helpers; the rate limiter reuses auth::bearer_token; the landing page
reuses invites::display_code. Removed: AppError::not_implemented (D-004 placeholder, only its own test used it) and
the unused Mutation::mutation_id/entity_type. tasks::start never awaited or failed: now a plain fn returning AppState.
Milestone-ownership comments (M2a/M3/M4) rewritten to say what the code does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
invites::create_partner_invite is the one partner-invite path of E-12 and invite-partner; E-3's user creation moves
into invited_user; invite kinds parse through rows::token; app_version_code comes from auth. cli::render_invite
replaces the two identical render functions and cli::with_database the open/run/close block of four subcommands.
Same output, exit codes and errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
api::rows now also loads settlements, audit entries and the epoch (sync::rows is gone; audit enums parse through
rows::token) and owns the expense, split, settlement and allocation inserts that sync push, recurring generation and
the importer each had their own copy of (rows::Origin carries the server-owned columns). accounts::insert_account and
replace_shares are shared with the importer. Recurring generation moves the per-period expense into
create_period_expense. No SQL, wire or log change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- SyncEngineTest: 429 stops with Retry(rate_limited) before pulling; 400, 413, 409, 422 push refusals are no
progress, the cycle pulls, succeeds and records last_error_code, one push only.
- WorkManagerSyncWorkTest on work-testing's test WorkManager: kvitt-sync APPEND_OR_REPLACE (second request chained),
kvitt-sync-periodic KEEP every 15 minutes, CONNECTED constraint. Adds androidx.work:work-testing (testImplementation,
same version as work-runtime).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- AppViewModel.onLink drops an invite link while paired (AC-2.6, D-084), so it never prefills S-1 later.
- PushTokens: a failed E-17 PUT (network, 429, 5xx) is kept by PushTokenRetry and sent again after each successful
sync until accepted; a newer FID replaces it (AC-13.7).
- Notifications are VISIBILITY_PRIVATE with a generic public version (notif_generic_title, notif_generic_body):
no description, amount, account or name on a secure lock screen (security review L-7).
- androidResources.localeFilters en, sv (SPEC G-8, AC-19.1).
- Tests: AppViewModelTest (links while paired and unpaired, AC-2.9 permission after the first finished successful
sync), PushTokenRetryTest, NotificationPrivacyTest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Rejection.lost: RejectedVersion (the pushed entity from sync_rejections.rejected_payload as a ledger row, account,
payer, split, notes, deleted); S-16 "Öppna" shows it read-only in a dialog with "Visa nuvarande version" (S-8/S-10);
without one it opens the current version as before. Ledger rows take a null onClick (read-only).
- AboutState.schemaVersion; AboutRepositoryImpl calls E-1 /health once per collection when online; header fallback.
S-22 shows "1.1.0 (schema 3)".
- SettlementRepository.start/recompute return null without a session or partner; save is Invalid(payer); no
checkNotNull/requireNotNull in :data.
- :feature:home screenshot harness: one more frame and an idle looper before each capture; the committed sv_dark,
200 % and dialog goldens were one variant stale (light instead of dark, en instead of sv_200).
- Tests: lost version (stale, refused deletion, unreadable payload), /health online and offline, settle without a
partner, category reorder PATCHes, recurring toWire splits, §9.2 effect cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Caddyfile: access and error logs replace the /i/<code> segment and every query string with REDACTED
(regexp filter on request>uri); Authorization stays redacted by default. Deferred response headers:
Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy.
- Pi compose: caddy as uid 10002 with group 1000, read-only root, cap_drop ALL plus NET_BIND_SERVICE
(needed to exec the image's file-capability binary), tmpfs /config and /tmp; install.sh sets /certs
0750 1000:1000 and Caddy's /data to 10002.
- deploy.sh and install.sh print the pulled image digests and append them to /opt/kvitt/deployed-digests,
warning when a recorded tag changed digest; the workflow prints the pushed digests; kvitt_cicd needs
only write:package.
- Dev stack: up.sh stages only fcm-service-account.json into infra/dev/secrets/ (git-ignored), mounted
read-only at /secrets.
- README: restricted cert-sync key required (plain root key not acceptable), remote_ip and log checks,
APK download note, owner decisions (backup encryption, APK limit, key form, token scope), overhaul lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- kvitt-server invite-partner: a partner invite by the admin as E-12 does (cancels the pending one, 72 h, link with
?from=<admin>), exit 2 household full, 3 no admin; README lines in server/ and infra/.
- Tests: invite-partner, migrate (schema at SCHEMA_VERSION, idempotent), healthcheck (200, 503, nothing listening),
serve SIGTERM drain (in-flight request finishes, new connections refused, exit 0; gives up after SHUTDOWN_DRAIN).
- Push sender: one info line "push sent" per message with push_type, devices_sent, devices_targeted and 8-character
device-id tails (SPEC §8.7 aggregates); per-device line stays at debug.
- tools/contracts-check.py: stale TODO(spec) replaced by the §9.11 CS-5 citation; shapes updated for invite_kind
(D-086), app_version_name (D-090) and fid (D-096), 134 checks pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-103 the infra/pi/ bundle (compose.yaml, .env.example, Caddyfile, install,
deploy, bootstrap, backup-now, install-apk; APK path via infra/pi/out/apk/;
new .env keys; gate docker compose -f infra/pi/compose.yaml config).
D-104 image names gitea.lan/jacobh2/kvitt-*, the Gitea workflow, Mac
fallback, supercronic, pinned versions. D-105 backup uid 10001 and
rclone.conf ownership (revises D-023 for that file). D-106 cert-sync tar
stream and refusals, backup confirm-then-delete. D-107 S-19 option rows.
D-108 instant navigation (design-spec §2.11). Chips cleanup and the
Dockerfile CARGO_BUILD_JOBS argument recorded without an id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AccountChip, PayerChip and SplitChip had no callers outside the debug
catalogue once S-7 and S-9 moved to option rows. The editor_chips sheet
keeps the date chip that S-9 still uses; its four goldens re-recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
NavHost enter, exit, pop and size transitions are None, so pushed screens,
tab switches, editors and the session gate change at once instead of the
default 700 ms cross-fade. NavTransitionsTest asserts the values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B