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
6.6 KiB
PLAN-W7: Server web serving and analytics endpoints
Status: running since 2026-10-21 from the orchestrator's brief (written before this plan), in parallel with W0. Where the running agent's brief and this plan differ, the brief governs this run and the W7 report records the difference.
1. Goal
Make kvitt-server serve the web client under /app/ from the image (D-132) and add the advanced analytics read endpoints under /analytics/* (D-133), as a contract change (D-135 to D-138, SPEC v0.29), with the image build moved to a repository-root context that includes a Node stage for web/.
2. Read first
- SPEC §15 (§15.2, §15.4, §15.8 especially), FR-21 (AC-21.1), §6.3, §7, §8.4, §8.6, §8.7, §9.5 (recurring dates), §10.3, §10.4, §10.5.2 (the
$kvitt_csppolicy for/app/), §14.3, §14.7, §14.8, D-117, D-126, D-128 (the shared 3-month window), D-129 to D-134. server/README.md,server/Dockerfile,.gitea/workflows/build-images.yaml,infra/build-images.sh,infra/dev/**,contracts/openapi.yaml,tools/contracts-check.py.CLAUDE.md(server gate, nounwrap/expect, contracts).
3. Already established
- Static serving (§15.4): when
KVITT_WEB_DIR(default/app) exists,GET /app/<file>serves the file; every other path under/app/servesindex.html;/appleads to/app/; without the directory/app/*is 404not_found. Hashed assets are cached as immutable,index.htmlno-store. The/app/responses carry a Content-Security-Policy equal to NPM's/app/value in §10.5.2 (or a stricter one the report justifies, with the NPM line updated by the docs agent to match), with the other D-117 headers. Path traversal is impossible (tests). The rate limits of §8.6 apply as to any unauthenticated path.KVITT_WEB_DIRgets a row in §10.4 (proposed in the report). - Analytics (§15.8): authenticated read endpoints under
/analytics/*(any device, §8.4): per-category averages and trends per month, overall and per category, and the forecast of the next 2 calendar months per account = sum of the account's recurring templates due in that month (active, not paused, inside start and end periods, the §9.5 clamped day) + the average per month of the account's non-recurring spend over the last 3 whole months, the window constant shared with D-128 (AnalyticsRules.MOVING_AVERAGE_WINDOW= 3 on the phone; the server's copy carries the same value and names its source). Names, query parameters, response shapes, rounding and the edge cases (an account younger than 3 months, archived accounts, deleted rows excluded) are this plan's design, written intocontracts/openapi.yamlwith examples and proposed as D-135 to D-138 and SPEC v0.29 text (§7 rows, §14.3.4 entries). Pure computations live inkvitt-domainif they are rules both clients could need, else inkvitt-server; this plan decides and says why. - Build (§10.3, §15.4):
server/Dockerfilegains a Node stage that runsnpm ciand the Vite build inweb/and copiesdist/to/appin the runtime image; the build context is the repository root (-f server/Dockerfile .);.gitea/workflows/build-images.yaml(sparse checkout addsweb, andcontractsif the web build reads it, path triggers addweb/**),infra/build-images.shandinfra/dev/**follow. The Node image is pinned by version and digest like the others. Whileweb/does not exist onmain, the stage must still build (or the plan sequences its landing after W0; the report says which). - A change to
contracts/openapi.yamlregeneratesweb/src/api/schema.d.ts(npm run gen:apiinweb/) in the same landing whenweb/exists (§15.3); it is the one file underweb/this plan may write. tools/contracts-check.pyfollows the new endpoints and examples (its endpoint list is in the file).
4. Owned paths
From SPEC §14.7 (row W7): server/**, contracts/**, tools/contracts-check.py, .gitea/**, infra/build-images.sh, infra/dev/**, and web/src/api/schema.d.ts for its regeneration only. Not infra/README.md, infra/npm/kvitt.conf, infra/pi/**, SPEC.md, docs/**.
5. Deliverables
- Static serving with integration tests (file, fallback, 404 without the directory, cache headers, CSP, traversal).
- The
/analytics/*endpoints in the server and incontracts/openapi.yaml(the OpenAPI equality test passes), contract examples, integration tests per endpoint and per error code, the forecast rule's cases (templates due, paused, ended, clamped day, no history, mixed recurring and one-off spend). - The Dockerfile, workflow,
infra/build-images.shandinfra/dev/**changes, checked by a local image build (linux/arm64on the Mac) that serves/app/and/health.
6. Steps
- Worktree: from
/Users/jhag/projects/kvitt,git worktree add .claude/worktrees/agent-w7 -b agent-w7 main; in itgit reset --hard main,tools/install-git-hooks.sh; copyinfra/secrets/files only if a dev-stack check needs them. - Read §2. Design the endpoints; write the contract; implement; test.
- Build the image locally (never while a Gradle build runs, CLAUDE.md); run it with a throwaway data directory; check
/app/and/health. - Gates (§7). Commit on
agent-w7,git rebase main; if W0 has landed meanwhile, regenerateweb/src/api/schema.d.tsand run the web gate too. Never merge, push or remove the worktree.
7. Gate
The server gate, from server/: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test; python3 tools/contracts-check.py; the web gate (npm run check in web/) when web/ exists on the rebased branch. Report the real result lines.
8. HUMAN steps
From SPEC §11.2: paste the v0.28 NPM block (infra/README.md step 4, with the key as in step 4a) and deploy the new image (§10.10), relayed by the orchestrator.
9. Out of scope
- The web client's pages (W0 to W5) and the Android Insights section (W6).
infra/npm/kvitt.conf,infra/README.md,infra/pi/**(docs agent and owner);SPEC.md,docs/decisions.md(proposed in the report).
10. Report
- Branch
agent-w7, head commit after rebasing; gate result lines; image build result (size,/app/and/healthchecks). - The endpoint design: paths, parameters, shapes, rounding, edge cases; proposed D-135 to D-138 entries and SPEC v0.29 text (§7, §10.3, §10.4
KVITT_WEB_DIR, §14.3.4, §15.4, §15.8); the exact/app/CSP and cache headers. - Contract files changed;
web/src/api/schema.d.tsregenerated or not; owner steps worded for relay; open questions with SPEC section numbers.
11. Numbering
D-135 to D-138, SPEC v0.29; server schema stays 3 (no migration expected; a schema change needs the orchestrator first).