The owner supplied the card picture and cleared its use for this private app
on 2026-10-24. Cropping to the card and the web copy follow with the K0 report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner request and answers of 2026-10-24: each user connects their own SJ Prio
Mastercard through Enable Banking (restricted production, BankID); only the
card holder sees the purchases; a fourth bottom destination "Kortköp"
(revises D-048); "Anslut kort" in Settings with the card artwork; one- and
two-tap linking to household expenses; web as a later plan.
FR-22, §9.12 (CP-1 to CP-14), server schema 4 (migration 0004), Room 2,
E-36 to E-44, S-23 to S-27, W-21 to W-23, OQ-7, OQ-8. PLAN-K0 describes the
test app of 2ce317f and the remaining run and report; K1 server, K2 Android,
K3 web. HANDOFF and prompts/README updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stdlib Python app (docs/spikes/k0/app.py) on localhost:8765: lists Swedish ASPSPs, starts BankID authorisation with the registered https redirect and a paste box for the landed address, creates the session, fetches transactions with and without PSU headers (continuation_key paging), balances, diff between fetches and an aggregate analysis block. Key and state in the git-ignored docs/spikes/k0/.local/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AC-3.2, E-16 (§14.3.4), §11.4: the "to implement" notes removed; D-156 says
implemented in 07c36d2; security review L-10 note: fixed in 07c36d2. ctutils
0.4.4 (RUSTSEC-2026-0385) in the v0.34 row. Server tests 300, contracts-check 136.
- §15.3: W0b as built (5305f37): shared pieces, no cross-feature imports or web_
keys, per-locale amount fields, Recharts chunk, Vitest cache off, version code.
Web gate 609 tests.
- server/Dockerfile: ARG KVITT_VERSION_CODE=1 in the Node stage, passed to
npm run build; the Gitea workflow computes git rev-list --count HEAD and passes
it as a build argument (§10.3).
- HANDOFF: main 5305f37 pushed 2026-10-24; next image build, deploy, laptop login,
browser checks; Web follow-ups closed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
A member naming the other user's device now gets the same 404 not_found
body as an unknown id, so E-16 no longer reveals whose a device id is.
The admin keeps revoking any device except the current one (AC-3.2, §8.4).
contracts/openapi.yaml drops the 403 on E-16 and states the rule;
web/src/api/schema.d.ts regenerated. The forbidden test becomes a
not_found test, plus one asserting the body equals the unknown-id body
and that nothing was revoked.
Also bumps ctutils 0.4.3 -> 0.4.4 (lockfile only) for RUSTSEC-2026-0385,
published 2026-10-23, which failed cargo audit on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-156: OQ-6 closed (no S-1 confirm step); L-10 not_found for the other
user's device (to implement: server answers forbidden, E-16 lists 403);
firewall rule on 8080 deferred; mem_limit kept as advisory; image builds
stay on the Pi runner, docker builder prune -af between builds; Samsung
Auto Blocker off for the in-app install (AC-16.3); AC-18.1 verified;
both phones on release 140; web first used from the laptop browser.
D-157: small edits by a Haiku or Sonnet agent in the main checkout.
infra/README.md owner section and HANDOFF updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-144 to D-154 record the web feature plans W1 to W5 (934eee7, a65ab71,
f9a9bfb, 1aecf51, 3ebe57d): preset account and payment in the query
string, Home empty state, the CSV format, AC-8.6 in the browser and
search in the URL, the expense editor's banner and controls, live
settle-up errors, analytics drill-down routes and chart accessibility,
W-18's route, the partner intro and About's fallback; D-150 unused.
D-155 records d686ee0: new AC-16.7, UpdateFailure in §14.2.4,
design-spec §3.6, S-17 and S-22. SPEC §11.2, §11.3, §15.3, §15.6 and
§15.7 follow the code; HANDOFF gets the state, the owner's browser
checks and the web follow-ups for a W0b plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Root cause of the raw keys (web_accounts_load_failed, the settle missing state) on main:
Vitest 5's file-system module cache (on by default) skips only modules whose source
contains the literal `import.meta.glob(`. catalog.ts wrote `import.meta.glob<…>(`, so
its transform was cached by its own content and kept the glob result from before a
feature added its i18n/*.json; the main checkout's node_modules/.vitest-cache served
that stale catalog until a config change invalidated it. Fresh worktrees passed.
- catalog.ts: glob without a type argument (typed by annotation), with the reason.
- catalog.test.ts: the loaded feature catalogs must equal the i18n files on disk.
- A string two features render moves to the shared catalog: web_accounts_load_failed
(accounts/PageStates, also rendered by Home) is now web_ledger_load_failed in
src/i18n/web-strings.csv; sv.json/en.json regenerated.
- vectors.test.ts: suggest_category is PORTED and runs against features/expenses/suggest.
- fakeServer: bodies by Content-Type (JSON parsed, text/* as text, else raw bytes);
ProfilePage's picture test asserts the uploaded bytes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- domain/: TypeScript port of resolveRange and spendSeries (contract vectors 13 + 13),
AnalyticsRules (averages, moving average, caption, filter rules, drill-down URL),
S-11/S-12 model from the sync snapshot, W-11 mapping of the E-34/E-35 responses.
- W-9 /app/analytics: period menu with custom range dialog, interval, measure, group-by;
total card; Recharts stacked bars (4/2 corners, 2 px segment gaps) with bucket links
as keyboard stops, legend, table view; category and account breakdowns.
- W-10 /app/analytics/:groupKind/:groupId (category, account, bucket): own period and
interval in the query string, header card, line chart with range average, 3-bucket
moving average and caption, expense rows.
- W-11 /app/insights: household and per-category trends, two-month forecast per account
with its recurring and average parts; online only, loading/error/empty states.
- Strings in features/analytics/i18n; render tests per page state, domain and chart tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
W-5 expense editor (S-7): amount, description, category quick row and
"Alla" grid with the §9.11 suggestion for a new expense (CS-1 to CS-7),
the four fixed option rows (D-102), split modes default, ratio, exact and
full with live shares, notes, inline §9.6 validation, one POST /sync per
save, delete with confirmation, the discard dialog on Cancel, Esc and
in-app navigation (D-080), rejection and network failures shown on the
page with the form kept (§15.5). Defaults per AC-8.6 with this browser's
last account and category; ?account= preselects the opener's account.
W-6 expense detail (S-8): hero, facts, shares and mode, recurring
template badge, the AC-12.4 timeline, deleted and missing states.
W-12 search with filters in the URL query, count and sum, rows a hundred
at a time, and the CSV export (UTF-8 BOM, semicolons, CRLF, decimal
comma, ISO dates, header in the page language), built in the browser.
The §9.11 port passes contracts/test-vectors/suggest_category.json (65
cases); the keyword table is a Vite raw import of
contracts/category-keywords.csv, which needs ../contracts in
server.fs.allow (vite.config.ts).
Shared editor fields of design-spec §3.7 in web/src/components/editor/
for W5's template editor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- W-13: household card (roles, partner missing), rows with category and recurring summaries, Invites for the
admin only, language and theme (W0's card), log out that forgets the token only (§15.2).
- W-14: picture upload (E-9, raw bytes, JPEG/PNG/WebP up to 5 MiB) and remove (E-10), name validation and save
(E-8); the S-2 step once per browser after a partner login (AC-2.10).
- W-15: list in sort_order with drag and keyboard reorder (arrow keys on the handle, move up/down) saved through
E-24 sort_order with restore on failure; archived section; editor with reset name, 33 icons, 14 colours,
archive/unarchive, delete or the reason from the snapshot (AC-7.6).
- W-16: list (account, payer, schedule, next date, paused badge); editor with D-072/AC-9.8 defaults, schedule,
pause/resume hints, create (E-28), patch of changed fields only (E-29), delete (E-30), discard dialog (D-080).
Uses minimal local fields until W2's shared editor fields land.
- W-17: devices (E-15) with this browser marked, revoke with confirmation (E-16), never the current device.
- W-18: create partner or device invites (E-12), code in groups of 4, link, expiry, copy to clipboard, pending
invites (E-13) with cancel (E-14); `/settings/invites` aliases W0's `/settings/invite`.
- W-19: web build version, server origin, server version from /health or the X-Kvitt-Server-Version header.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
W-7 (/app/settle, mirrors S-9): global, per account (?account=<id>) and
editing (?settlement=<id>). Defaults per AC-11.3/11.4/11.6, proposal from
proposeAllocation over all non-deleted accounts with nets excluding the
edited payment, rows follow the proposal until one is edited, "Föreslå
igen", swap, over-settle warnings and confirm dialog (AC-11.5), remaining
indicator, add account, untouched info line (D-093), date and note,
§14.5.3 errors live, one POST /sync mutation, §14.4.6 rejections shown
with a re-pull, discard dialog on leaving with changes (D-080).
W-8 (/app/settlements/:id, mirrors S-10): hero, date, note, allocations,
audit timeline (AC-12.4), deleted and missing states, edit link and soft
delete with confirmation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Home (mirrors S-4, S-3 while the partner has not joined): global balance hero
with the seesaw and §9.3 wording, Settle up, active accounts with balance and
this month's count, collapsed "Arkiverade (n)" kept for the session, empty,
loading and error states, New expense when an active account exists.
Account detail (S-5): header with default split, split bar, archived badge and
balance; expenses and settlements interleaved by date, grouped by month with
household spend (settlement rows carry the allocation, AC-11.7); overflow menu
with Edit, Archive/Unarchive (undo snackbar, D-071) and Delete only when
allowed (AC-6.5); not-found, loading and error states.
Account editor (S-6): name (1-60 code points, name_taken on the field), two
percent fields that always sum to 100 with two decimals, slider and presets,
the "Av 100,00 kr" preview; discard dialog with a title and no body (D-080)
on every navigation away; E-18/E-19 apply the returned row.
The account preset for W-5 and W-7 is the query parameter `account`
(proposed D-144). Tests over a shared snapshot fixture and the fake server.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
v0.30: D-140 S-22 "Check for update" calls UpdateRepository.checkNow(), which always asks
GET /apk/latest and restarts the 6-hour window (AC-16.2, AC-16.6, S-22, §14.2.4).
v0.31: D-141 InsightsRepository UI contract (§14.2.4); D-142 Insights placement and loading
on S-11; D-143 display rules; design-spec S-11 "Insights" and §3.10; strings.csv 588 rows
(16 analytics_insights_* keys), web dictionaries regenerated; §11.2 W0 and W6 landed.
HANDOFF: state at 1c9856b, Pi image deploy step, numbering (D-144..D-155 reserved, next v0.32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The PackageInstaller session's final status now reaches UpdateRepository:
InstallResultReceiver hands it by session id to the waiting AndroidApkInstaller
call (ApkInstaller.install returns a PendingInstall). installOutcome() maps
STATUS_FAILURE_BLOCKED to UpdateFailure.Blocked, STATUS_FAILURE_ABORTED back to
Available without an error, and every other failure to UpdateFailure.Install
with the installer's EXTRA_STATUS_MESSAGE. The cached APK is deleted once the
installer answers, so "Try again" downloads again. UpdateBanner's Failed variant
shows the reason on S-4 and S-22; two core:design strings (en/sv) with their
docs/ux/strings.csv rows. Unit tests for the mapping, the repository outcomes
and the receiver; goldens: update_banner re-recorded, S-17-install-blocked,
S-17-install-failed and s22_about_install_blocked new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
InsightsRepository in :core:model (SpendTrends, SpendForecast and parts); :data DTOs for E-34/E-35
(round-trip the contract examples, fixing ExampleCoverageTest), InsightsRepositoryImpl through KvittApi and
OnlineGate with category/account names from Room (deleted rows included); FakeInsightsRepository with
scenarios (loaded, empty household, account without history, offline, unreachable, long); the Insights
section below S-11's charts: household trend and per-category trends on SpendLineChart with average and
moving average, a forecast card per account, "Needs connection" / "Could not reach the server" over the last
data of the session. Strings sv/en; 41 goldens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every
fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript
types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for
the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or
cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC
§14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to
web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css
(light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from
docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-135 /app/ serving from KVITT_WEB_DIR (fallback, caching, CSP, path safety); D-136 /app on the
unauthenticated per-IP limit; D-137 repository-root image build with the Node 24 stage and the allowlist
.dockerignore; D-138 E-34 trends and E-35 forecast with the rules in new §14.5.7; D-139
publish-apk.sh --deploy. infra/README.md build context; HANDOFF state and numbering (next D-141, v0.31).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
UpdateRepository.checkNow() (default: check(), for throttle-free fakes) skips
the 6-hour throttle of the automatic check (AC-16.2) and restarts its window;
AboutViewModel calls it. Debug builds still never check (AC-16.5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Part A (D-132): KVITT_WEB_DIR (default /app) served under /app/ by a small handler: /app redirects to
/app/, app routes fall back to index.html, a missing assets/* file is 404, assets cached immutable,
index.html no-store, the app's own CSP plus nosniff, DENY and no-referrer. Unsafe paths never reach the
file system. /app paths count against the unauthenticated per-IP limit. Without the directory every /app
path stays 404 not_found. server/Dockerfile gains a Node 24 LTS stage (pinned by digest) building web/,
passed through the Rust stage so npm and rustc never run at once on the Pi 3; the build context is the
repository root with an allowlist .dockerignore (server/.dockerignore removed); workflow, build-images.sh
and infra/dev/build.sh follow.
Part B (D-133): GET /analytics/trends (E-34) and GET /analytics/forecast (E-35), authenticated, read-only,
SQL month buckets; rules (half-up average, trailing 3-month moving average, percent against the rounded
average, template occurrence per month) in kvitt-domain::analytics. OpenAPI, examples, contracts-check
and tests updated (35 operations).
Cargo.lock: rustls-webpki 0.103.16 and webpki-root-certs 1.0.10 (RUSTSEC-2026-0382, -0383) so that
cargo audit passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
New SPEC §15 Web client and FR-21; §1, §2, G-5, AC-14.1 revised; §8.5 and
§10.5.2 gate lines for /app/, the invite accept and bearer requests (except
/health, /setup/, /i/, /apk/) plus the /app/ CSP via $kvitt_csp, identical in
infra/npm/kvitt.conf and infra/README.md step 4 (checked with nginx 1.31.7);
§10.3, §10.6, §11.1 to §11.3 (rows W0 to W7), §14.7 ownership; change log is
now §16. PLAN-W0..W7 and prompts/W0..W7; HANDOFF numbering D-139 / v0.30.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
A dedicated forced-command key (scp -t into data/apk only) copies
kvitt-<code>.apk then latest.json with legacy scp (-O), host key pinned in
infra/secrets/pi_known_hosts on first use, then curls
https://$KVITT_PUBLIC_HOST/apk/latest with X-Kvitt-App (fed on stdin) and
requires the new version_code. Without --deploy nothing changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
New AC-14.11; §13.3 S-12 row with its own range and granularity, the line
chart and the empty-range text; §13.4 strings.csv 569 rows. design-spec
§3.10 SpendLineChart and LineLegend, AnalyticsFilterBar row 2 optional,
S-12 layout for a category or account over the range. strings.csv gains
the seven analytics keys as built in 12e2508. HANDOFF: D-128 and the
keyboard fix landed, web-client programme holds D-129+ and v0.28+.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
A drill-down opened from a breakdown row now shows its own period chip and
Month/Week toggle (initialised from S-11, S-11 keeps its own filter), the
header total, a line chart of the group's spend per bucket with the dashed
range average, the trailing 3-bucket moving average and a legend, and a
caption comparing the latest full bucket with the average; the expense rows
follow. Bucket drill-downs are unchanged. The series is the group's column of
AnalyticsState.buckets from spendSeries, so no contract change.
:core:design chart: SpendLineChart and LineLegend beside StackedBarChart,
shared axis/slot helpers (bar chart pixels unchanged), AnalyticsFilterBar
measure and group-by toggles optional.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AC-15.3, §9.9 and §13.3 (S-4, S-5) name the pull as a sync now, not
disabled offline. design-spec: S-4's "Pull-to-refresh is not offered"
replaced; S-4/S-5 refreshing frames, interactions and TalkBack;
KvittPullToRefresh in §3.6; ContainedLoadingIndicator row in §3.1;
spatial fast and the named motion in §2.11. HANDOFF: D-127 landed
(3778f5f), Pi live, both phones paired, import done; next free D-129 /
v0.28 after the parallel D-128 (v0.27).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The recurring, category and account editors drew their scrolling body
inside KvittScaffold with only the scaffold padding, so with adjustResize
and edge-to-edge the keyboard overlaid the content and a focused field
(S-19 day of month, notes) stayed hidden. Each body now applies
padding(padding).consumeWindowInsets(padding).imePadding(), as S-7 does
with imePadding(): the viewport shrinks above the keyboard and Compose's
scroll container keeps the focused text field in view. Consuming the
scaffold padding first stops the navigation inset (or the S-19 keypad
bottom bar) from being counted twice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
KvittPullToRefresh in :core:design wraps Material 3's PullToRefreshBox with
Kvitt's own indicator: a primary-container disc (the hero's first-sync tile
look) with the sync icon, which winds with the pull while a primary arc fills
to the threshold, pops at the threshold with a GestureThresholdActivate haptic,
and turns into an indeterminate ring while the sync runs. Material 3 1.4.0 has
no LoadingIndicator or LinearWavyProgressIndicator, so the circular indicator
stands in; the swap happens in Refresh.kt only.
Home (S-4) and account detail (S-5) call syncNow() of SyncRepository, the path
of S-16's "Synka nu"; the indicator stays until it returns, success or failure.
Offline the pull ends at once without a sync. TalkBack reads "Synkar…"
(sync_syncing) while refreshing; no new string keys.
New goldens S-4-home-refreshing and S-5-account-refreshing; ViewModel tests
with GatedSyncRepository (core:design test fixtures).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Outside the home LAN NPM closes every request without X-Kvitt-App: <key>
(return 444); the LAN check reads $realip_remote_addr (NPM's X-Real-IP
realip makes $remote_addr forgeable) and Let's Encrypt's challenge passes
on the normalised $uri. Block rehearsed in NPM v2.16.0's openresty.
README: step 4a (key, keyed APK, gate, acceptance test), health checks,
stale CARGO_BUILD_JOBS text and owner decisions fixed. HANDOFF as of
2026-10-21.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The reverse proxy closes requests from outside the home LAN that lack
X-Kvitt-App: <key>. The key is the build property kvitt.appKey
(-Pkvitt.appKey, else android/local.properties, else empty = no header),
validated as 16-128 printable ASCII without whitespace, exposed as
BuildConfig.APP_KEY and passed via AppBuildInfo.appKey to KvittApi
(every call, /health and other unauthenticated calls included) and the
Coil picture client (PictureAuth). AppBuildInfo.toString hides the key.
Tests use a made-up key; README documents the property.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-125 (owner decision 2026-10-20, landed 6e5981d): 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 of 3820f9c. 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
The Kvitt Pi runs Kvitt from /home/jacobhagstedt/kvitt: compose.yaml, .env,
secrets/ and relative bind mounts ./data, ./backups, ./import. Both containers
run as the owner's uid/gid (KVITT_UID, KVITT_GID). No install script, no /opt,
no Docker daemon or system changes; operations are plain docker compose
commands (README "Pi: what the owner does", RESTORE.md).
Removed install.sh, deploy.sh, bootstrap.sh, backup-now.sh, install-apk.sh,
lib.sh. .env holds only the keys compose.yaml reads; RCLONE_REMOTE is fixed
to gdrive: in compose.yaml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
KVITT_PI_HOST and KVITT_PI_SSH_USER were read by nothing on the Pi (D-103).
They are gone from .env.example; the README's scp, ssh and NPM steps use
<user>@<kvitt-pi-address> and the Kvitt Pi's LAN IPv4 address in words.
install.sh checks only the keys the stack reads, so an older .env that still
has the two keys installs unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Checkout is a cone-mode sparse checkout of server and infra/backup (the
two build contexts), which makes actions/checkout fetch with
--filter=blob:none. fetch-depth stays 0: with depth 1, fetch-tags only
auto-follows tags on the fetched commit, and the version step needs
every v1.0.* tag. The temporary secrets diagnostic step is gone and the
header names kvitt_cicd as the owner's user-level secret.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-124 (owner decision 2026-10-19; a86ad24, 4852803, 038c603): Kvitt is served
at https://<host> with no explicit port; the router's existing external 443
forward to the Home Assistant Pi serves it, no 8443 rule. Every :8443 in SPEC
replaced; D-120 to D-123 struck through with pointers. The pinned-root config
is generated from its template with kvitt.publicHost (038c603). HANDOFF,
prompts/README.md status and the S-1 TalkBack line follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-124 serves Kvitt at https://<host> on 443. The README, the AppBuildInfo and
AboutRepositoryImpl comments, the S-21/S-22 fake scenarios and the :data test
fixtures drop the :8443 port. S-21's invite-created goldens and every S-22
golden re-recorded (link and server row now portless). The release-URL
normaliser test checks a portless URL and keeps one explicit-port case (a
typed port survives), on example.org instead of the owner's old host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Pinned-root network security config (D-082) is now a template in
app/src/releasePinnedRoot/; with kvitt.tls.pinnedRoot=true the task
generatePinnedRootNetworkSecurityConfig writes it into the release
resources with the domain kvitt.publicHost (D-121), and stops with a
clear message while the root src/release/res/raw/kvitt_root_ca.crt is
not committed (fallback unprepared, SPEC 10.5.3). Comment: pinned NPM
root (D-120).
- Port 443 (D-124): RELEASE_SERVER_URL is https://<kvitt.publicHost>
without a port (RELEASE_SERVER_PORT removed); the AC-2.6 intent filter
has no android:port; InviteLink drops an explicit default port.
- S-1 fixture GOLDEN_SERVER https://kvitt.example.org; 28 S-1 goldens
re-recorded (only the server row changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST} with no port; the health check
through NPM prints the port-less URL; publish-apk.sh's summary drops :8443.
README: no new router rule (the existing external 443 forward to the Home
Assistant Pi serves Kvitt, NPM selects by name), NPM listens on 443, the
fallback, operations and APK-abuse notes, and every :8443 URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
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
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
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