Commit Graph

114 Commits

Author SHA1 Message Date
jacobh2 a86ad241f1 Server and contracts: public URL on port 443 (D-124)
Kvitt is served on the default HTTPS port through Nginx Proxy Manager, so the
OpenAPI server is https://{host} (contracts/openapi.yaml and openapi.rs
PUBLIC_SERVER_URL), examples E-7 public_url and E-12 link are
https://kvitt.example.invalid without a port, tools/contracts-check.py expects
the portless header, and the server's tests, config message and README name
no port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 20:22:09 +02:00
jacobh2 cce3e4e9d9 SPEC v0.22: D-122 S-22's release server and publish-apk host; D-123 no built-in server host; Kvitt host name
D-122 (4a4e45f, 4b1ae0c): AppBuildInfo.releaseServerUrl, S-22 before pairing, publish-apk.sh reads
KVITT_PUBLIC_HOST from infra/pi/.env. D-123 (c9abd24, e381e37): KVITT_PUBLIC_URL required,
KVITT_TRUSTED_PROXY defaults to none, OpenAPI server https://{host}:8443, E-7/E-12 examples on
kvitt.example.invalid. The owner's host name kvitt.suorra.duckdns.org recorded in HANDOFF (SPEC §10.1,
§10.5.1 point there). HANDOFF and prompts/README.md status at ac17b41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 20:19:00 +02:00
jacobh2 ac17b41a3e install.sh: the NPM proxy host is README step 6
Build Kvitt images / build (push) Failing after 56s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 20:11:46 +02:00
jacobh2 e381e371ea contracts-check: OpenAPI header expects the {host} server variable (D-123)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 20:11:46 +02:00
jacobh2 c9abd24c77 Server: no built-in public host, no trusted proxy by default; OpenAPI server variable host (D-123)
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
2026-10-19 20:11:46 +02:00
jacobh2 4b1ae0c473 publish-apk.sh: build for KVITT_PUBLIC_HOST from infra/pi/.env (D-121)
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
2026-10-19 20:09:31 +02:00
jacobh2 4a4e45fcd9 Android: S-22's unpaired server from AppBuildInfo.releaseServerUrl (D-122)
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
2026-10-19 20:09:31 +02:00
jacobh2 dd07ca4395 Gitea workflow: temporary secret diagnostic step
Build Kvitt images / build (push) Failing after 55s
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
2026-10-19 20:06:54 +02:00
jacobh2 4bcff2cf89 SPEC v0.21: D-120 Nginx Proxy Manager instead of Caddy and cert-sync; D-121 Android host property
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
2026-10-19 20:05:49 +02:00
jacobh2 d090c6c850 Android: public host as the build property kvitt.publicHost (D-121)
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
2026-10-19 19:58:58 +02:00
jacobh2 9231a1d755 infra README: owner decisions list after the M-1 answer (SPEC v0.20)
Build Kvitt images / build (push) Failing after 49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 19:51:10 +02:00
jacobh2 62f5279daa infra README: Nginx Proxy Manager proxy host, router to NPM, NPM fallback (D-120)
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
2026-10-19 19:50:28 +02:00
jacobh2 d05f13a080 Pi stack behind Nginx Proxy Manager: remove Caddy and cert-sync (D-120)
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
2026-10-19 19:50:28 +02:00
jacobh2 4f69ae010e Gitea workflow: registry login with the secret in the run line (env form came through empty on act_runner 0.6.1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 19:50:28 +02:00
jacobh2 f1abb0d2ef Server gate: cargo audit
Build Kvitt images / build (push) Failing after 51s
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
2026-10-19 19:36:54 +02:00
jacobh2 481889a9dc SPEC v0.20: D-119 APK download rate limit; owner answers to the security review; HANDOFF, prompts
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
2026-10-19 19:35:24 +02:00
jacobh2 96e4cbdf4e Server: APK download rate limit 10/min per IP (D-119)
Build Kvitt images / build (push) Failing after 55s
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
2026-10-19 11:15:26 +02:00
jacobh2 3ef2c15764 SPEC v0.19: D-118 Android overhaul (shared test fixtures, :core:design limits and failures); HANDOFF, prompts, hash remap in plans
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 23:21:59 +02:00
jacobh2 c58236338c Android overhaul: one configureAndroidModule for the application and library plugins; coil-compose out of :app
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
2026-10-18 23:13:12 +02:00
jacobh2 74b5fbdb59 Android overhaul: S-7's state types in ExpenseEditorState.kt
ExpenseEditorViewModel.kt (555 lines) held the editor's form, state, overlay and event types beside the ViewModel;
they move unchanged to their own file (471 + 97 lines).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 23:13:12 +02:00
jacobh2 a92c236689 Android overhaul: stale comments and the module README
* 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
2026-10-18 23:13:12 +02:00
jacobh2 0714242954 Android overhaul: the §9.6 text and date limits in :core:design's Limits.kt
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
2026-10-18 23:13:12 +02:00
jacobh2 e08aaf099b Android overhaul: remove dead code (tokens, an icon, catalogue and fake samples, helpers, unused imports)
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
2026-10-18 23:13:12 +02:00
jacobh2 515b60fc65 Android overhaul: shared failure texts, saved-split label and limits in :core:design
* 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
2026-10-18 23:13:12 +02:00
jacobh2 8229ed60ea Android overhaul: one screenshot harness in :core:design's test fixtures; kvitt.android.screenshots wires Roborazzi
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
2026-10-18 23:13:12 +02:00
jacobh2 e4e6e5d792 Android overhaul: one CopyCategoryKeywords task in build-logic instead of a copy in :data and :core:fakes
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
2026-10-18 23:13:12 +02:00
jacobh2 48fe119ae1 SPEC v0.18: D-109 to D-117; validation reports; HANDOFF, prompts
Build Kvitt images / build (push) Failing after 1m24s
- 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
2026-10-18 22:51:42 +02:00
jacobh2 b985ce969d E-17: fcm_token at most 512 characters (security L-4)
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
2026-10-18 22:43:54 +02:00
jacobh2 baa5806812 Pictures: at most 16 megapixels and 64 MB decoded per upload (security M-3)
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
2026-10-18 22:43:54 +02:00
jacobh2 725901c98f Rate limiter: a token is accepted only after a 2xx; unaccepted tokens count against the IP (security H-1)
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
2026-10-18 22:43:54 +02:00
jacobh2 21407456f1 Server overhaul: sync push delete step in its own function; clap env feature dropped
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
2026-10-18 22:43:54 +02:00
jacobh2 c942d73c66 Server overhaul: clear clippy::pedantic wins
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
2026-10-18 22:43:54 +02:00
jacobh2 8cb42f85a9 Server overhaul: one test harness in tests/common for every test binary
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
2026-10-18 22:43:54 +02:00
jacobh2 6c405e142d Server overhaul: shared helpers, dead code, synchronous tasks::start, stale comments
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
2026-10-18 22:43:54 +02:00
jacobh2 4b30888dc2 Server overhaul: invite creation and CLI plumbing shared
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
2026-10-18 22:43:54 +02:00
jacobh2 8103dae2be Server overhaul: one place for stored-row reads and the shared inserts
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
2026-10-18 22:43:54 +02:00
jacobh2 a0259ab68d M12f: remove the 19 string keys R8 found unused (20 CSV rows); §9.11 tie-break comment
Removed from the sv/en resources and docs/ux/strings.csv (562 rows, header included): common_archived_category,
editor_amount_hint, editor_split_half_each, editor_split_preview_effect, editor_next, analytics_range_dates,
analytics_chart_spoken, analytics_axis_thousands, expense_detail_title, expense_detail_account,
home_notification_rationale, home_sync_status_all_synced_body, home_sync_status_tag_new, home_sync_status_tag_changed,
home_sync_status_tag_deleted, home_sync_status_row_pending, settings_devices_summary (one, other),
settings_pending_badge, settle_empty_settled. CategorySuggestion cites §9.11 instead of a stale TODO(spec).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:33:07 +02:00
jacobh2 c366fbd9a3 M12f: :data tests for the 429 retry, D-040 whole-request refusals and WorkManager unique work
- 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
2026-10-18 22:33:07 +02:00
jacobh2 2bd6b584f8 M12f: :app invite links dropped while paired, push-token retry, private notifications, en/sv only (D-112, D-116)
- 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
2026-10-18 22:33:07 +02:00
jacobh2 5b2ed37f65 M12f: :feature:account screenshot harness captures the current variant (sv_dark goldens were light)
One more frame and an idle main looper before each capture, as in :feature:home; S-5 sv_dark re-recorded dark, S-6
text-field cursor frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:33:07 +02:00
jacobh2 dc7a296d70 M12f: S-16 lost version read-only, S-22 versions from /health, settle start without a partner (D-109 to D-111)
- 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
2026-10-18 22:33:07 +02:00
jacobh2 7fb86f5524 Infra security fixes: Caddy log redaction, headers and non-root container; dev mounts only the FCM key; deploy records image digests (D-117)
- 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
2026-10-18 22:24:32 +02:00
jacobh2 8660b7fa16 Server gap pass: invite-partner subcommand, CLI and drain tests, push info log (D-113, D-114)
- 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
2026-10-18 22:12:28 +02:00
jacobh2 e70fe0d9fe Images: --provenance=false on buildx pushes; workflow passes the token via env, registry in one place, git version sort, temp builder config; build-images.sh shares the Gradle lock check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:11:07 +02:00
jacobh2 b647faf715 infra/pi: install.sh keeps the deployed KVITT_TAG and checks python3; env_set cleans its temp file; backup.sh removes half-written archives on failure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:11:07 +02:00
jacobh2 fc6126c6af infra/dev: env_value defaults match compose, die helper, shared Gradle lock check; drop unused RCLONE_REMOTE and M2a leftovers; fix stale Pi compose path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:11:07 +02:00
jacobh2 61f92c81c5 pre-commit hook: Android tests through gradle-serial.sh with JDK 25 on PATH, JAVA_HOME untouched; installer replaces an older Kvitt hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:11:07 +02:00
jacobh2 2fac1ecba0 tools: one Gradle lock helper (gradle-lock.sh) for gradle-serial.sh and clean-builds.sh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:11:07 +02:00
jacobh2 18c527f018 Goldens and UX mock-ups: restored after the history rewrite that dropped superseded PNG versions (owner decision 2026-10-18, D-098)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 22:01:07 +02:00
jacobh2 400eac9d60 SPEC v0.17: D-103 to D-106 Pi bundle, D-107 S-19 option rows, D-108 instant navigation; HANDOFF, prompts
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
2026-10-18 21:56:55 +02:00