Skip to content

release: promote beta to main - #2041

Merged
steilerDev merged 42 commits into
mainfrom
beta
Aug 7, 2026
Merged

release: promote beta to main#2041
steilerDev merged 42 commits into
mainfrom
beta

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Release Summary

Promotes 40 commits from beta to main: the bank report wizard reaches feature completeness (column-visibility passthrough, continuation-row handling, editable-field limits, locale-correct PDF rendering), auth rate limits become configurable, a rate-limiter IPv6 normalization CVE and two dependency advisories are remediated, and budget drill-downs become deposit-aware.

Changes

Features

Fixes

Security

Budget

Reports

Other

Chores / Refactoring

Docs

Change Inventory

232 files changed, +17,447 / −6,002.

Backend (server/, shared/)

  • server/src/services/llmGateway/** — renamed gateway, fixtures, plain-prose enforcement
  • server/src/services/shared/**computeIncludedTotal and shared helpers
  • server/src/routes/**, server/src/plugins/** — auth rate limiting, config validation
  • server/src/db/migrations/**
  • shared/src/types/**subsidyProgram, sourceReport, invoice, plus shared/src/lib

Frontend (client/)

  • client/src/lib/reportContent/**, client/src/lib/reportPdf/** — report content and PDF pipeline
  • client/src/pages/ReportWizardPage/**useReducer refactor, override fields
  • client/src/components/reports/**, client/src/components/EditableField/**, client/src/components/SourceBudgetLinePanel/, client/src/components/calendar/**, client/src/components/GanttChart/
  • client/src/pages/SubsidyProgramsPage/**, client/src/pages/InvoicesPage/**, client/src/pages/BudgetSourcesPage/
  • client/src/i18n/en/**, client/src/i18n/de/**

E2E Tests (e2e/)

  • e2e/pages/InvoicesPage.ts, e2e/pages/ReportWizardPage.ts, e2e/fixtures/auth.ts
  • e2e/tests/budget/reportWizardEditableContent.spec.ts, e2e/tests/invoices/*, e2e/tests/diary/*, e2e/tests/admin/search-users.spec.ts, e2e/tests/navigation/dashboard.spec.ts

Docs / Config

  • docs/src/getting-started/configuration.md, docs/src/getting-started/docker-setup.md
  • .github/workflows/ci.yml, .github/workflows/release.yml
  • scripts/ci-wait.sh, scripts/board.sh, scripts/hooks/bash-guard.mjs, scripts/hooks/bash-guard.test.mjs, scripts/i18n-audit.mjs
  • CLAUDE.md, package.json, package-lock.json, wiki submodule

Security Findings Summary

Three advisories resolved in this range and none outstanding: CVE-2026-15144 (rate-limiter IPv6 normalization bypass — the custom keyGenerator allowed per-address limits to be evaded from an IPv6 /64), GHSA-g4rg-993r-mgx8 (undici credential leak / SSRF), and GHSA-rhx6-c78j-4q9w (brace-expansion).

Manual Validation Checklist

Testing

  • DockerHub beta image: docker pull steilerdev/cornerstone:beta
  • PR-specific image: docker pull steilerdev/cornerstone:pr-<pr-number>

steilerDev and others added 30 commits August 3, 2026 21:16
…1946)

- Widen `guardedUpdate` dirty predicate to include `isGeneratingAi` — in-flight generation now triggers the discard confirmation on any context change
- Add monotonic `aiGenerationTokenRef` token; confirmed discards bump it so the in-flight result is silently dropped on arrival
- Token-guard `finally` block so a stale generation cannot clear a live generation's spinner
- Conditional modal title/body copy for in-flight-only case; clear `skippedDocuments`/`aiError` in handler callbacks

Fixes #1946

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
…d rows (#1965)

- Reinstate explanatory legend sentences for split (partial) and deposit-reduced rows in the report PDF
- Fix JSX whitespace between footnote marker and text for preview/PDF rendering parity
- Invert E2E Scenario 18 to assert one deduplicated legend entry; update stale POM directives
- Strengthen unit test assertions: marker equality, i18n text, whitespace-parity guard on <li>
- Correct ADR-034 to document document-level deduplicated legend model (vs. per-row footnotes)

Fixes #1965

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…t; remove dead isLoaded API (#1972)

- Failed PATCH calls from the column-settings popover now surface an error toast instead of silently dropping the failure
- Local column state is preserved on error so a subsequent toggle carries the full intended set
- Removed dead `isLoaded` boolean from `useColumnPreferences` (maintained but never consumed by any production code)
- Added `ToastProvider` wrapper to eight page-level test render helpers that were missing it after `useColumnPreferences` gained a context dependency

Fixes #1972

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
…1938)

- Pages 2+ of generated report PDFs now show the complete generated-at line with label and timestamp; previously the timestamp was blank on every multi-page report (#1938)
- German table header labels "Auftragnehmer" (67.5pt > 45pt column) and "Rechnungsbetrag" (78.7pt > 48pt column) now shortened to "Firma" and "Betrag" — eliminating mid-word breaks on every German-locale report (#1937)

Fixes #1938
Fixes #1937

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
- Replaced deposit-blind `getWorkItemLineInvoiceData` / `getHouseholdItemLineInvoiceData` helpers with deposit-aware `getInvoiceAggregates` from `budgetServiceFactory`
- `hasClaimedInvoice` now uses a status-existence check (`rows.some(r => r.invoice_status === 'claimed' || r.deposit_status === 'claimed')`) rather than an amount threshold — correctly handles claimed invoices fully covered by non-claimed deposits
- Fixed rider ternary: `new Set([status])` (was coercing non-'claimed' to 'paid')
- Updated `wiki/API-Contract.md` field notes for `hasClaimedInvoice` and `actualCostPaid`

Fixes #1897

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…ve (#1971)

- Rewrote `Search filters by email` to use worker-scoped `testPrefix`, seeded match + non-match users, and bidirectional filtering assertions
- Added universal-negative loops to `Search is case-insensitive` and `Search updates results dynamically` verifying every rendered row matches the query term (name and email cells, matching `UserManagementPage.tsx`'s filter)
- Added `fullRows.length ≤ partialRows.length` narrowing assertion to the dynamic test
- Used `Date.now()` suffix in seed email to avoid collisions with deactivated-but-not-deleted prior rows

Fixes #1971

Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
…uth (#1966, #1969)

- Fix column-visibility E2E coverage for `budget-overview` (7-column variant incl. Status) — Scenario 24 in `reportWizardEditableContent.spec.ts`
- Decouple `testPrefix` fixture from `authenticatedPage` dependency so tests not needing auth don't incur the overhead
- Add ESLint suppression in auth.ts for Playwright's required empty-destructuring syntax
- Positive-control assertion confirms route interceptor fires before "nothing fired" assertion

Fixes #1966
Fixes #1969

Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
…nt (#1913, #1952)

- Replace expiring `'2027-06-15'`/`'2027-06-01'` absolute date literals in two test files with `futureDateStr(500)` relative helpers — fixtures self-renew every run
- Add `stripMarkup()` to LLM response validator: strips bold/italic (CommonMark flanking guards), ATX headings, bullet markers, numbered markers (run-of-≥2 only, preserving German ordinals/dates), and HTML tags from `letterBody`, `letterSubject`, and `descriptions[].description` before length truncation — 46 unit + integration tests at 100% function coverage

Fixes #1913
Fixes #1952

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…tWizardPage to useReducer

- **#1967** — Remove dead `attachmentsNote` override from `applyOverrides.ts` and `overrideKeys.ts`; field has been static read-only text since PR #1959, no UI path can produce its override key; adds regression test pinning the removal
- **#1947** — Refactor `ReportWizardPage` from 38 `useState`/`useRef` hooks to a `useReducer` state machine in `wizardReducer.ts`; staleness now enforced via opaque request-id tokens (M1/M2 fixes); named tier types with explicit factory return-type annotations enforce AC4 at compile time; 57 unit tests at 100% coverage; behaviour-preserving (existing tests unchanged per AC3)

Fixes #1967
Fixes #1947

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
- Added `AUTH_RATE_LIMIT_MAX` and `AUTH_RATE_LIMIT_WINDOW` env vars to configure the login endpoint rate limit (defaults: 20 requests / 15 minutes)
- Invalid values (zero/negative max, zero-magnitude or malformed window) cause startup failure with a descriptive error — no silent fallback
- Setup route remains hardcoded with a code comment explaining why; wiki docs updated

Fixes #1970

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…updates

Remediates Dependabot bump of github-actions group (docker/login-action 4.5.1→4.6.0, docker/scout-action 1.23.1→1.24.0, github/codeql-action/upload-sarif patch). Security-hardening and bugfix releases only — no breaking changes, no new permissions. See PR description for full changelog.
… 4 updates

Remediates Dependabot bump of prod-dependencies group: @fastify/rate-limit 11.2.0 (security release GHSA-grpc-p53c-r64v), better-sqlite3 13.0.2 (SQLite engine 3.53.4 segfault fix), fastify 5.11.0 (Content-Type parsing fix, async hook hardening), react-router-dom 7.18.2 (patch).

Note: the @fastify/rate-limit bump is necessary but does not fully close CVE-2026-15144 for this codebase — a custom keyGenerator in rateLimitPlugin.ts bypasses IPv6 normalization. Remediation tracked in #1995.
- Remediates webpack dual-instance crash (5.108.4 + 5.109.2 coexisting → single 5.109.2)
- Dev-dependency bumps: prettier, @types/*, concurrently, ts-jest, typescript-eslint
- `npm dedupe` fixes lockfile nesting from Dependabot's `--package-lock-only`

Fixes #1976-related webpack TypeError

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
- Remediates GHSA-rhx6-c78j-4q9w (brace-expansion ReDoS, Critical)
- Bumps brace-expansion 5→5.0.9, 2→2.1.4, 1→1.1.18 across all ranges
- Lockfile-only change via `npm update brace-expansion`
… floor

- Records #1973 column-visibility rulings and the rejected mandatory-floor
- Records Rev 3 spec reconciliation: three-tier summary-label fallback, AC 3.7 chunk-budget clamp
- Documents process failure: always say "body rewritten" after an issue-body revision

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude product-owner <noreply@anthropic.com>
…SRF)

- Remediates GHSA-g4rg-993r-mgx8 (undici credential leak/SSRF)
- Override strategy: forces sub-7.29.0 transitive instances to 7.29.0 while allowing testcontainers to use 8.x
- Replaces blanket `"undici": "7.28.0"` override that pinned below the patched threshold
…d, fix wiki debt

- Rename `budgetExtraction/` → `llmGateway/` to reflect its broader LLM role
- Switch gate from deprecated `autoItemizeEnabled` → `llmEnabled` in `llmGateway/index.ts`
- Extract `computeIncludedTotal` to `shared/src/lib/reportMath.ts` (used by server + client); replace O(n²) `Array.includes` with `Set.has`
- Fix `sourceId!` non-null assertion → proper early-return guard; add `GenerateReportContentResponse` type annotation
- Correct `shared/src/types/sourceReport.ts` JSDoc for `budgetLines[]` scope
- Wiki: fix ADR-034 claims, update API-Contract `LLM_NOT_CONFIGURED` gate flag, correct Security-Audit paths

Fixes #1917
Fixes #1914

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude security-engineer <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
- Adds "No Category" as a selectable option in subsidy applicable categories
- Includes subsidy service factory updates and test fixtures for `includesNoCategoryItems`
- Fixes E2E smoke test mock missing `claimable`/`quotationCoveredByDeposits` fields
- Rebased onto latest beta (includes Claimable tile, auth rate limits, etc.)

Fixes #1983

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
…on (CVE-2026-15144)

- Removes the custom keyGenerator that opened IPv6 normalization bypass (CVE-2026-15144)
- Restores @fastify/rate-limit's built-in IPv6 normalization

Fixes #1970

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…, null guard, and unit tests (#1995)

- Extract `rateLimitKeyGenerator` as a named export with shared `IPV6_SUBNET = 64` constant
- Add null guard (`?? 'unknown'`) and explicit `normalizeIP` call to fix CVE-2026-15144 identity-gate bypass
- Add unit tests covering null-IP, /64 normalization, and subnet isolation
- Fix wiki (API-Contract.md, Architecture.md) to document the actual keyGenerator implementation

Fixes #1995

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude security-engineer <noreply@anthropic.com>
…ls injection (#1993)

- Add `pageLabel: string` to `ReportContentLabels` and populate it via `reportT` in `buildReportContent.ts`
- Replace `t('sourceReports.table.pageLabel')` in `merge.ts` with `reportContent.labels.pageLabel`, making the footer locale contract injection-only (hardening, not a bug fix — the production caller always passed `reportT`)
- Update seven test files that construct `ReportContentLabels` literals; add regression assertion in `merge.test.ts`
- Update ADR-034 wiki: mark the known-open-violation resolved, add Deviation Log row

Fixes #1993

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
#1968)

- Routes the grey areaText/attachmentsNote suffix in the Usage column through buildUsageTextRuns, applying wordBreak: break-all per token — matching the protection already applied to body prose (#1968 AC1/AC2)
- Relaxes splitUsageCell test helpers from "exactly one grey run" to "contiguous grey runs at the tail", concatenating their text so all existing assertions remain unchanged
- Adds regression tests that fail when the production hunk is reverted: multi-grey-run count (unit) and wordBreak: break-all on grey run (unit + real-render)

Fixes #1968

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
- Add contextual note paragraph in ReportInvoiceList showing which report type's attachments are displayed (#1888)
- Add HTML lang attribute to report-language content nodes in ReportContentEditor when report language differs from UI locale; counter-tag UI chrome (EditableField reset button, sr-only hint) with uiLang (#1910)

Fixes #1888
Fixes #1910

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
…eakdown contract (#2005)

- Fix `mockInvoicesFullSummary()` E2E fixture: add missing `claimable` and `quotationCoveredByDeposits` fields, rename `pagination.total` → `totalItems` — corrects `InvoiceStatusBreakdown` contract compliance
- Root cause of shard 8/16 red: `InvoicesPage` crashed with `TypeError` on `summary.claimable.count`, preventing integration-status from loading and `?create=1` from opening any modal

Fixes #2005

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
…ment) (#2001)

- Removed `TFunction` parameter from `buildCoverLetterContent`, `buildOverviewContent`, and `generateReportPdf` — `reportPdf/*` now reads all locale-resolved strings exclusively from `ReportContentLabels`, making ADR-034's locale-decoupling contract compiler-enforced
- Added `coverLetterReferenceLabel`, `coverLetterSubjectLabel`, and `skipReasonLabels: Record<ReportSkipReason, string>` to `ReportContentLabels`; populated in `buildReportContent.ts` via `reportT`; `ReportSkipReason` is the single authoritative union used by `SkippedDocument.reason`, `buildOverviewContent`, and `merge.ts`
- AC7 sentinel tests in `coverLetterPdf.test.ts`, `overviewPdf.test.ts`, and `buildReportContent.test.ts` discriminate: they fail if any string is sourced from `TFunction` instead of `labels.*`

Fixes #2001

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…rview PDF (#2003, #1980)

- Replace vacuous `maxHorizontalRatio <= 1` assertions with falsifiable per-cell `_minWidth <= _calcWidth` checks that flip red on three independent mutations: threshold change, column-width change, and removing `wordBreak: 'break-all'`
- Add legend-sentence layout tests: both sentences present (en/de), exactly one occurrence, no-flag report renders neither, two deposit-reduced invoices deduplicate to one footnote entry
- Fix E2E NBSP in `(less deposit)` assertion; correct ADR-034 rule #1 (3rd Deviation Log entry)

Fixes #2003
Fixes #1980

Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…enerated PDF (#1973)

- Generalize `overviewPdf.ts` from two hardcoded table shapes to a computed geometry engine covering all 96 legal column subsets, with Allocated Amount as the only mandatory column; a single absorber column (Usage, else Vendor) takes the slack so the table never exceeds `printableWidth()` and renders narrower rather than stretched when no free-form text column is visible.
- Lift `hiddenColumns` out of `ReportContentEditor` into the wizard reducer as per-run state that resets on use-case change and is never persisted to the server; add `client/src/lib/reportContent/columns.ts` as the single derivation consumed by both the editor UI and the PDF builder.
- Add the locked Allocated Amount checkbox with an accessible explanation and a `.bannerWarning` shared class for the Usage-hidden-with-attachments warning; delete the superseded `columnVisibilityHint` string from both locales.

Fixes #1973

Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
Co-Authored-By: Claude ux-designer <noreply@anthropic.com>
…m line splits (#1911)

- Add `SourceReportInvoice.splitKind` (`'lines' | 'deposits' | 'both' | null`), derived inside the existing step-f UNION at no extra query cost. The predicate is "this arm contains a source ≠ the requested one", not "this arm contains ≥2 distinct sources" — the latter returns null for the headline case and reproduces the bug. Fixes the filed defect: an invoice whose lines are all in source A but which carries a deposit tagged to source B was byte-identical in the response to a genuine A/B line split.
- Fix a second, unfiled defect found during AC refinement: `isDepositReduced` was also over-inclusive, firing on a line-split invoice carrying an untagged deposit. Its legend sentence claims the deposit is claimed "separately", but untagged deposits are apportioned back into this source pro-rata — so shipped bank reports carried a false statement. The E2E suite had encoded that bug as expected behaviour.
- Remove the unsound `budgetLines.length > 0` gate, which discarded a correct server-side split signal whenever none of the split's lines belonged to the requested source.

Refs #1911

Note: `Refs`, not `Fixes`, deliberately. #1911 carries a UAT disposition, and this cluster is parent-less — so promotion reaches `main` through standalone `/release`, which omits the UAT sections. `Fixes` would auto-close the issue at promotion with the appearance of a validation that never ran.

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude security-engineer <noreply@anthropic.com>
… gate (#1992)

- Remove the `OIDC_REDIRECT_URI` row from the auth env-var tables in `wiki/Architecture.md` and `wiki/API-Contract.md`. The server never reads it — the full 32-variable `getValue(...)` read-set in `config.ts` contains no such name — so an operator who set it got no effect.
- Correct the "OIDC is enabled when all four OIDC variables are set" claim on both pages. The gate is three (`config.ts:142`), and an operator who correctly set only those three had reason to believe they had misconfigured something.
- Replace the removed row with `EXTERNAL_URL`, now the load-bearing variable for the callback URL, and document how the URL is actually derived (`oidc.ts:45`) — including that **both** halves of the request-host fallback are gated on `TRUST_PROXY`, so a correct scheme alone does not make the fallback safe.
- Close the `2026-08-04` Deviation Log entry that had flagged this as an unresolved follow-up from PR #1989, correcting its own wrong claim that `CLAUDE.md` shared the discrepancy. Add a Deviation Log convention to both pages: correct forward, never rewrite a Deviation cell — the log records what we believed and how we got it wrong, which is the only thing that distinguishes it from a changelog.

No production code changes.

Refs #1992

Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
steilerDev and others added 10 commits August 6, 2026 02:56
- Document `AUTH_RATE_LIMIT_MAX` (default `20`) and `AUTH_RATE_LIMIT_WINDOW` (default `15 minutes`) on the docs site, closing the docs-site half of #1970's AC5. Describes the accepted duration shape directly rather than linking `vercel/ms`, since `config.ts:365` validates a hand-rolled regex enforcing a strict subset — `900000`, `1y` and `1 msec` are all valid `ms` input and all rejected at startup.
- State the real failure modes: a malformed value aborts startup (the throw at `config.ts:377` propagates out of `configPlugin` registration, and nothing on the path to `app.listen` catches it), while a value beginning with digits is truncated by `parseInt` rather than rejected — `20abc` becomes `20` and `2e3` becomes `2`. #1991 will tighten the latter; the copy states what is true today.
- Explain the `TRUST_PROXY` connection an operator needs: with it unset behind a proxy, `request.ip` resolves to the proxy for every visitor and collapses them into one rate-limit bucket. Notes that only the nearest hop is trusted, so a CDN-in-front-of-proxy chain still buckets by the CDN edge.
- Describe both deployment shapes with a tie-breaker for the overlap: a home instance exposed via port-forward matches both, and should be treated as internet-exposed, since raising the max to cure household lockouts also raises the ceiling for an attacker sharing that bucket.
- Add a pointer from `docker-setup.md`'s proxy note, which governed only *whose* IP is trusted but read as though rate limiting behind a proxy were fully handled.

Fixes #1990

Co-Authored-By: Claude docs-writer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
- Replace the template-literal i18n key interpolation in `getAttachmentNote` with an exhaustive `ATTACHMENT_TYPE_KEYS: Record<AttachmentType, string>` map, so a fourth `AttachmentType` member becomes a compile error rather than a raw i18n key printed onto a bank-facing PDF. Narrows the parameter from `string | null` to `AttachmentType | null` and drops an `as string[]` cast — that widening is why the guard could not previously be written.
- Extract `toBcp47Locale()` as the single source of truth for locale to BCP 47 mapping, applied at all six duplication sites rather than the two the issue named. A helper four sites bypass makes divergence less visible, not more.
- Make `reportFormatters` required on `buildReportContent` and delete the six silent fallbacks that rendered raw ISO dates and unformatted amounts. Dead in production, but the optional signature let a future caller ship an unformatted bank PDF.
- Name `createFormatters`'s return type as `AppFormatters extends Formatters`, making the subset relationship a compile-time contract; rename `.step4Body` to `.step5Body` and `sourceReports.stepper.options` to `.actions`, both of which named step 4 while describing step 5; split `.settingsDivider`'s double duty via `composes`; fix the `deducedTypes` typo.

Adds coverage for the `deposit` attachment type, which had none anywhere in the repo. The new map tests were verified falsifiable by scrambling the map, independently by two agents.

Fixes #1912

Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
…2030)

- Scope the `waitForResponse` predicate in three diary specs to the `type` query parameter each test asserts on, and read the value off the resolved `Response`. The previous shape captured intercepted requests into a shared array, reset it, then awaited any `/api/diary-entries` 200 — but the handler pushes at request time while the predicate resolves at response time, so an already-in-flight request could resolve the wait before the chip's own request was intercepted, leaving the array empty.
- Correct the comment claiming `waitForLoaded()` guaranteed the response was captured. It guarantees the UI settled, not that nothing is in flight, and that reasoning is what made the race look impossible.
- Found in `diary-automatic-events.spec.ts` while triaging a red shard on an unrelated PR, then swept: `diary-list.spec.ts` and `diary-r2-uat.spec.ts` carried the identical bug. Confined to the diary suite.

Fixes #2030

Co-Authored-By: Claude e2e-test-engineer <noreply@anthropic.com>
…ows (#1940)

- Merge runt trailing chunks into the row before them, so a text whose tail lands just past a chunk boundary no longer produces a row blank except for a single stray character — which reads as document corruption on a document whose purpose is to look credible.
- Mark continuation rows with a leading ellipsis run, so a row with every leading column blank is distinguishable from the orphaned-cell bug rather than identical to it. Ink shape rather than colour, deliberately: this is a bank document that gets scanned, and colour is what degrades under greyscale printing.
- Resolve AC1's tension with AC2 by gating the reduced-budget repack behind an actual runt check, so the zero-degradation range is byte-for-byte unchanged and no ceiling is re-derived. The safety bound holds algebraically at any cascade depth, verified by fuzzing 400,000 cases with zero violations.
- Express the floor per-subset as `max(MIN_CONTINUATION_ROW_FLOOR_CHARS, usageSafeTokenChars)`, so a merged runt fills at least one real line at whatever width the active subset gives Usage.

`splitIntoPageSafeChunks` and `packUsageCellRows` are byte-identical to beta — the fix wraps their row-level output, because the acceptance criterion's unit is the rendered row while the chunker's unit is a chunk within one segment.

Fixes #1940

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude ux-designer <noreply@anthropic.com>
… fields (#1941)

- Add an optional `maxLength` plus three pre-translated hint strings to the shared `EditableField`, and wire per-field limits at all eight report-wizard call sites. Absent props keep today's unbounded behaviour exactly, so no existing or future consumer is forced to opt in.
- Expose the constraint two ways rather than one: an always-mounted static description announced on focus, before the user is near the limit, plus a separate polite live region carrying text only while at the limit. The visible counter is `aria-hidden` and appears at 90% — on its own it would leave screen-reader users with nothing.
- Treat the over-limit state as informational, never an error. Such a value arrives from AI-generated cover-letter content or derived usage text, never from the user typing past the cap, so it displays in full with no `aria-invalid`, no error colour and no blocking — and the copy avoids obligation vocabulary in both locales.
- Refactor `ariaDescribedBy` from a ternary into a filtered-array composition, which a two-id description requires. The edited-hint gate stays label-only on purpose: dense mode already conveys edited state through the accessible name, so describing it there would announce it twice.

Each of the seven limits carries its own anchor rather than a bare number. `usageText` is 500 rather than the initially proposed 150 because the baseline is derived, not typed — a single budget line already admits a legal 500-character value, so a lower cap would make the over-limit state the routine state.

Fixes #1941

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
Co-Authored-By: Claude translator <noreply@anthropic.com>
Co-Authored-By: Claude ux-designer <noreply@anthropic.com>
…_SIZE (#1953)

- Give the cover letter's subject line its own font-size constant with its own literal, rather than reusing `SUBHEADER_FONT_SIZE`. That constant is load-bearing arithmetic: `headerFootprint()` consumes it and the result drives `PAGE_TOP_MARGIN`, so shrinking it to reclaim header space would have silently shrunk the subject line on an approved document, and bumping the subject line for legibility would have reflowed every page of every report.
- An alias would have satisfied the naming complaint while preserving the exact coupling that is the problem, so the new constant is genuinely independent. The ux-designer spec that directed the reuse argued against a magic literal, not that the two sizes are the same thing — its reasoning for the subject line never mentions the running header.
- Remove the now-inverted comment directing the reuse, and record the `PDF_STYLES` split trigger in the module header: `letterSubject` is the first style entry with no geometry consumer, a split waits for the second, and the direction is `pageGeometry <- pdfStyles <- merge`.

No behaviour change — `headerFootprint()` and `PAGE_TOP_MARGIN` are byte-identical. The two sizes are now pinned independently in tests, verified by mutation in both directions.

Fixes #1953

Co-Authored-By: Claude frontend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-architect <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
- Replace bare `parseInt` with one shared `parseStrictInteger` helper at all eight numeric env-var call sites, so `20abc`, `20.9`, `1e3` and `' 20'` are rejected by name at startup instead of silently becoming `20`, `20`, `1` and `20`. The server no longer starts with a different limit than the operator wrote.
- Preserve every existing bound and range check byte-identically, and keep errors accumulating into the single aggregated throw so an operator with three typos learns about all three in one startup attempt.
- The helper permits an optional leading sign rather than the issue's original sign-less form. A sign-less regex would reclassify `PORT=-1` from "must be in range 0-65535" to "must be a valid number" — which is false, since `-1` is a valid number that is merely out of range. The criterion was amended after the product-owner enumerated all eight call sites and confirmed every one rejects negatives via its own bound, so none can reach a config field.

No behaviour change for any currently-valid configuration: no existing test needed modification.

Fixes #1991

Co-Authored-By: Claude backend-developer <noreply@anthropic.com>
Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
…1950)

- Recompute both derived Uk ceilings from the geometry constants rather than re-typing them, so `MAX_SAFE_USAGE_CHUNK_CHARS`'s documented 34-character exceedance fails loudly instead of going stale. The ceiling depends on `USAGE_WIDTH_7COL`, `TABLE_BODY_FONT_SIZE`, `TABLE_SMALL_FONT_SIZE` and `DEFAULT_LINE_HEIGHT`; before this, any of them could move while nothing failed and the only record of the exceedance was a comment a refactor could delete.
- Pin only the measured line budgets as literals, labelled as real-render measurements that cannot be derived. Each of the four constants was individually mutated to confirm the guard fires, and the failure message names the constant that moved and points at the rationale comment rather than reporting a bare number.
- Account for #1940's continuation-row marker, which adds two rendered characters no budget counts because it is applied post-packing. The base-row overage of 34 chars / 3 lines / 33.6pt becomes 36 / 4 / 44.8 there, and the extra line is bounded by asserting the marker is shorter than one line's capacity.
- Pin #1941's editor-renderer coupling — the usage-text input cap must stay below the renderer's 7-column budget — asserted from the rendered `maxlength` attribute, so no production export was needed.

Test-only: no production value changes and the rationale comments are untouched. The issue's AC 1.5 was struck on amendment, since the constant it names was removed by #1959 the day after the issue was filed; satisfying it literally would have meant restoring the exact thing this guard exists to keep from silently returning.

Fixes #1950

Co-Authored-By: Claude qa-integration-tester <noreply@anthropic.com>
- Agent-memory updates from the tail of the #1950 amendment, recording that premises must be re-verified at pickup rather than trusted from the filing date when a queue holds several items reworking the same files.

Co-Authored-By: Claude product-owner <noreply@anthropic.com>
Rework the 11-agent system for lean single-session delivery: fix-or-block review policy (2-round cap, deferrals need filed issues), CI-parallel reviews with a single merge-time ci-wait, dev-team-lead session continuity, once-per-run wiki sync, opus/sonnet/haiku rebalance.

Agent definitions slimmed 2,864 -> 591 lines against CLAUDE.md; static formats extracted to .claude/templates/; new scripts/i18n-audit.mjs (surfaced 18 real missing-key bugs); bash-guard push-regex hardened with behavioral tests; ci-wait stale-check-run fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@steilerDev

Copy link
Copy Markdown
Owner Author

Detailed Validation

Step-by-step instructions for the changes in this promotion. Run against steilerdev/cornerstone:pr-2041 (or :beta).

1. Report column visibility flows into the PDF (#1973)

  1. Go to Budget → Reports and start the report wizard for a source with at least a dozen budget lines.
  2. In the column configuration step, toggle off two or three columns (e.g. a usage column and a date column).
  3. Advance to the preview — confirm the hidden columns are gone from the on-screen preview.
  4. Generate/download the PDF.
  5. Expect: the PDF table contains exactly the visible columns. The remaining columns expand to fill the page width — no leftover gap where a hidden column was, and no column overflowing the page margin.

2. Continuation rows and runt merging (#1940, #1911)

  1. Pick (or create) a source whose report table spans more than one page.
  2. Generate the PDF.
  3. Expect: where a table continues onto the next page, the continued rows are visibly marked as continuations. No page starts with a single stranded "runt" row — such a chunk is merged into the previous page.
  4. If the source has a deposit-driven split, confirm the split row is labelled as deposit-driven rather than as an ordinary line split.

3. Editable override fields respect length limits (#1941)

  1. In the report wizard, open an editable override field (subject, intro text, attachment note).
  2. Paste or type text well past the allowed length.
  3. Expect: input is capped at the limit with the limit surfaced in the UI. Generate the PDF and confirm the text was not silently truncated at a different, surprising point.

4. German locale rendering (#1937, #1938, #1993, mixed-locale preview)

  1. Switch the UI language to Deutsch.
  2. Generate a report PDF.
  3. Expect: the running-header timestamp is formatted in German. Long German compound words wrap within their cell rather than overflowing it. The page footer text is German (not a leaked English fallback).
  4. With German UI but English report content (or vice versa), check the preview — the mixed-locale block carries the correct lang attribute, so hyphenation/word-break follows the content's language.

5. Legend sentences reinstated (#1965)

  1. Generate a report containing both a split row and a deposit-reduced row.
  2. Expect: the legend beneath the table explains both cases in full sentences — neither case is silently unexplained.

6. Deposit-aware budget drill-down (#1897) and actual invoiced amounts

  1. Go to Budget → Sources and pick a source funding an invoice that was partly covered by a deposit.
  2. Drill into the source's lines.
  3. Expect: each source line shows the actual invoiced amount, and the "used" total for the source matches the sum of those lines. Deposit-covered portions are not double-counted.

7. Subsidy "No Category" option

  1. Go to Manage → Subsidy Programs and create or edit a program.
  2. In applicable categories, select No Category.
  3. Save, navigate away, and reopen the program.
  4. Expect: "No Category" is still selected. Budget items with no category are covered by the program's calculation.

8. Configurable auth rate limits (#1970, #1990, #1995) and env validation (#1991)

  1. Start the container with AUTH_RATE_LIMIT_MAX=3 and AUTH_RATE_LIMIT_WINDOW=60.
  2. Submit four bad logins in under a minute.
  3. Expect: the fourth is rate-limited (HTTP 429), i.e. the configured maximum is in force rather than the built-in default.
  4. Restart with AUTH_RATE_LIMIT_MAX=3.5.
  5. Expect: the server refuses to start with a clear config error rather than silently coercing the value.
  6. If reachable over IPv6: confirm that requests from different addresses within the same /64 share a rate-limit bucket (this is the CVE-2026-15144 fix — previously each address got its own bucket, making the limit trivially evadable).

9. Column-preference save failures surface (#1972)

  1. Open any data table (Invoices, Budget lines) and let it load.
  2. Stop the server (or block the network) and toggle a column's visibility.
  3. Expect: an error toast reports the failed save. The toggle does not silently appear to succeed and then revert on reload.

10. AI generation guarded against use-case changes (#1946)

  1. In the report wizard, start an AI generation.
  2. While it is still in flight, switch the report use case.
  3. Expect: the in-flight result does not land in the new use case's fields — it is discarded or clearly scoped to the request that started it.

Regression spot-checks

  • Invoices list, filtering, and the overdue view load and filter correctly.
  • Diary list and automatic events render; diary type chips filter as expected.
  • Dashboard loads with no console errors.
  • Admin → user search returns matches by email.

- Add the Bank Report Wizard guide (the feature had no docs page) and register it in the sidebar
- Correct the stale subsidies guide to match the real SubsidyProgram type (multi-select categories, No Category, 5-value status enum)
- Add AUTH_RATE_LIMIT_MAX, AUTH_RATE_LIMIT_WINDOW, and VAT_RATE to .env.example, with defaults verified against config.ts
- Refresh README capabilities and rewrite RELEASE_SUMMARY.md for this release

Co-Authored-By: Claude docs-writer <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.14.0-beta.27 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@steilerDev steilerDev closed this Aug 7, 2026
@steilerDev steilerDev reopened this Aug 7, 2026
@steilerDev
steilerDev merged commit d64c40f into main Aug 7, 2026
173 of 197 checks passed
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.14.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant