Skip to content

release: promote v2.8.0 to main#1721

Merged
steilerDev merged 51 commits into
mainfrom
promote/v2.8.0
Jun 16, 2026
Merged

release: promote v2.8.0 to main#1721
steilerDev merged 51 commits into
mainfrom
promote/v2.8.0

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Release Summary — v2.8.0

Supersedes #1700 (whose required CI checks would not re-fire on the beta head branch after an auto-fix [skip ci] push — a webhook-delivery issue). This PR promotes the identical content from a pushable head branch (promote/v2.8.0, byte-identical to beta HEAD 9d4f3b78) so CI fires reliably. Merge with a merge commit to preserve individual commits for semantic-release (→ minor v2.8.0).

Note: the non-required Enforce Promotion Source guard (#1717) will report red because the head branch is promote/v2.8.0 rather than beta; this is a one-time recovery for the webhook-delivery issue and the content is identical to beta. The guard is not a required check.

Features

Fixes

Chores

Incident recovery included

Reconciled mainbeta (#1716) after PR #1710 was mistakenly squash-merged into main; main is now a clean ancestor of beta.

Testing

  • DockerHub beta image: `docker pull steilerdev/cornerstone:beta`

steilerDev and others added 30 commits June 13, 2026 13:33
…d duration to daily log (#1673)

* feat(diary): add optional vendor and work start/end time with computed duration to daily log

Extends daily_log diary entries with three new optional fields: vendor
(via SearchPicker), work start time, and work end time (both HH:mm 24h
inputs). The server resolves vendorName from vendorId for display. A
computed duration (hours, 2 decimal places) is calculated client-side
and shown inline between the time inputs. Cross-field validation rejects
end ≤ start. DiaryMetadataSummary renders the new fields for saved
entries. German translations and full unit + E2E test coverage included.

Fixes #1672

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
Co-Authored-By: Claude backend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude translator (Sonnet 4.6) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>

* test(diary): add computeWorkDuration to formatters mock factories

DiaryMetadataSummary now re-exports computeWorkDuration from formatters.js,
so any test file that mocks formatters.js and transitively imports
DiaryMetadataSummary must include the export or the suite fails with a
SyntaxError. Add computeWorkDuration to the mock factories in DiaryPage.test.tsx
and DiaryEntryDetailPage.area.test.tsx (matching the style of each file's
existing computeActualDuration stub).

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…items (#1678)

Previously, when auto-itemizing an invoice, line items where `includesVat`
is `false` (net prices) were summed at their raw stored value without
applying the applicable VAT rate. This caused the computed auto-itemize
total to be ~19% lower than the actual gross amount, producing a false
variance on the invoice and triggering a spurious `TOTAL_MISMATCH` error
during itemized-sum validation.

The fix introduces a shared helper `effectiveLineAmount(line)` in
`@cornerstone/shared` that returns the gross-equivalent amount for any
line: `amount * (1 + vatRate)` for net lines and `amount` for gross
(includesVat=true) lines. All total-computation paths in
`invoiceAutoItemizeService` now use this helper. Storage is unchanged —
line amounts are still persisted as net; the VAT gross-up is applied
only at aggregation time.

Fixes #1677

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
#1684)

Hoisted 52 inline `import()` type annotations to top-of-file `import type`
declarations across 23 test and page-object files (14 client, 4 server, 5 e2e).
No runtime behavior change — purely mechanical lint compliance.

Fixes #1458

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…ollision (#1685)

Add an exact-version override (10.3.0) for the `konva` package in the root
`package.json` so the entire workspace tree hoists a single Konva copy.
Without this override the root package had no Konva entry while
`@cornerstone/client` depended on it directly, allowing npm to silently
resolve two different Konva versions under certain install conditions and
causing TypeScript type-collision errors.

Also adds `scripts/check-single-dep-version.sh`, a guardrail script that
asserts exactly one version of a given package is present across the
`node_modules` tree. The script is wired into the Static Analysis job in
`ci.yml` so that any future Konva version drift fails CI immediately.

Fixes #1646

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…rlay (#1680)

Replaces the hidden context-menu unlink action with a visible overlay button
on LinkedDocumentCard that appears on hover/focus. Also fixes a bug where the
unlink-confirmation modal's Cancel button rendered a raw i18n key instead of
the translated string (was using `t('button.cancel')` without the required
`common:` namespace prefix).

Fixes #1680

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…re usable (#1688) (#1689)

Fixes #1688

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…arnings (#1469) (#1690)

Adds justified inline eslint-disable directives (with -- reason comments) and
renames stale `react-hooks/exhaustive-deps` directives to the real rule
`@eslint-react/exhaustive-deps` across 45 client/src/ files.

All changes are comment-only or behavior-neutral dependency-array reformats;
no logic or rendering behavior is altered. The inline disables are explicitly
permitted by the issue's acceptance criteria where refactoring would require
non-trivial state-model changes.

Verified result: zero remaining set-state-in-effect errors, zero
@eslint-react/exhaustive-deps errors, zero stale-rule "Definition for rule
not found" errors, zero unused-disable directives. Net ESLint error count
drops by 20 vs beta HEAD with no new errors introduced.

Fixes #1469

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…-pattern (#1568) (#1686)

* test(infra): fix per-render useNavigate jest.fn() anti-pattern in LinkedDocumentsSection

Replace `useNavigate: () => jest.fn()` with a stable module-scope mock
(`const mockNavigate = jest.fn()`) that is cleared in beforeEach.

The per-render pattern allocates a new function object on every component
render/re-render within a test, which inflates Jest worker heap over time
when the test suite has many renders. The stable module-scope mock reuses
the same function across all renders in a test run.

This is Phase 4 from issue #1568 (Jest memory audit). Phases 2-3 (shared
formatters mock factory via static import) were investigated but cannot be
delivered: adding a static `import` before `jest.unstable_mockModule` in
Jest 30 ESM VM mode causes the mock to fail to intercept the real module in
CI (shards 1, 2), even though the factory itself is sound. The inline factory
pattern is required for reliable mock registration in Jest ESM.

Fixes #1568

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.6) <[email protected]>

* chore(memory): document Jest ESM static-import mock constraint from issue #1568 investigation

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.6) <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude qa-integration-tester (Sonnet 4.6) <[email protected]>
…1463) (#1691)

Replace `any` with `unknown` or precise types where the shape is known,
and mark genuine boundary escape hatches (Fastify decorators, dynamic
drizzle table rows, schema-erased DB handles, WebDAV inject verbs) with
reasoned per-line eslint-disable comments. Type-only changes; no runtime
or test-behavior changes.

Fixes #1463

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude backend-developer (Haiku 4.5) <[email protected]>
Added a Node-scripts flat-config block in eslint.config.js (Node globals
+ no-console: off for scripts/**) ordered after the General rules block,
clearing 171 no-undef/no-console problems in scripts/*.mjs.

Real fixes in server/src (calendarIcal, diaryService: no-useless-assignment),
shared/src (diary.ts: no-empty-object-type interface→type alias), and
client/src (no-unused-vars, no-case-declarations, preserve-caught-error
cause, IIFE-in-JSX extraction, useState setter renames).

Justified inline disables in client/src where no stable id exists for
keys, render-time `new Date()` is intentional, DOM-measurement
useEffect/setState patterns are correct, and naming collisions exist in
LocaleContext/ThemeContext. Test file disables cover
error-boundaries/rules-of-hooks edge cases. Dead-variable removals in
e2e/tests (no-useless-assignment, no-unused-vars, no-self-assign) with
no behavior or assertion changes.

Full-project `eslint .` now reports 0 errors and 0 warnings (down from
857 at the start of the #1455 tracking issue; all 16 child issues
already closed).

Fixes #1455

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
…h, jsx-no-children-prop) (#1696)

- Rename local `async function fetch()` in useTimeline.ts to `loadTimeline`
  to eliminate the false-positive @eslint-react/web-api-no-leaked-fetch
  warning (the function is not the Web Fetch API, but shares the name)
- Add justified eslint-disable-next-line for LinkedDocumentsSection.tsx
  where `systemLinkedIds.fetch()` is a custom hook method, not the Web
  Fetch API — the rule cannot distinguish call sites by type
- Move `children` from the React.createElement props object to the 3rd
  positional argument at two call sites in PhotoMetadataSidepanel.test.tsx
  to fix jsx-no-children-prop; behavior-identical refactor

All changes are behavior-preserving. `eslint .` now reports 0 errors and
0 warnings across the full client workspace, completing the clean-lint
baseline established by #1455.

Ref #1455

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
… orientation metadata (#1674)

Mobile-first photo upload: Take photo / Upload photo split on touch devices, per-photo metadata capture modal (description, hierarchical area, orientation) with non-blocking background upload queue, new user-configurable Orientation entity (settings CRUD + nullable photos.orientation_id FK, SET NULL on delete), OrientationPicker showing name + description, orientation field in the photo metadata sidepanel, and the orientation-selector empty-state hint. Fixes #1674. Fixes #1675.
#1681)

Adds a Paperless-first invoice creation flow on Budget → Invoices: when Paperless + LLM are configured, "New Invoice" opens a document picker (searchable correspondent filter, hide-already-linked default ON, open-in-Paperless link, manual-entry escape). Selecting a document runs a stateless LLM auto-itemize preview (LLM picks the vendor from the app vendor list), and the invoice + document link + budget-line itemizations are created atomically only after human review.

New endpoints: GET /api/paperless/correspondents, POST /api/invoices/auto-itemize/preview, POST /api/invoices/auto-itemize/commit (ADR-032). No DB schema changes.

Fixes #1679
…1693)

Bumps the github-actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.36.1 to 4.36.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@87557b9...8aad20d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the dev-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.8.12` | `5.8.19` |
| [concurrently](https://github.com/open-cli-tools/concurrently) | `10.0.1` | `10.0.3` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [semantic-release](https://github.com/semantic-release/semantic-release) | `25.0.3` | `25.0.5` |
| [stylelint](https://github.com/stylelint/stylelint) | `17.12.0` | `17.13.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.1` | `8.61.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `25.9.3` |


Updates `@eslint-react/eslint-plugin` from 5.8.12 to 5.8.19
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.8.19/plugins/eslint-plugin)

Updates `concurrently` from 10.0.1 to 10.0.3
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](open-cli-tools/concurrently@v10.0.1...v10.0.3)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](prettier/prettier@3.8.3...3.8.4)

Updates `semantic-release` from 25.0.3 to 25.0.5
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](semantic-release/semantic-release@v25.0.3...v25.0.5)

Updates `stylelint` from 17.12.0 to 17.13.0
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](stylelint/stylelint@17.12.0...17.13.0)

Updates `typescript-eslint` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint)

Updates `@types/node` from 25.9.1 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.8.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: concurrently
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: semantic-release
  dependency-version: 25.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: stylelint
  dependency-version: 17.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… 8 updates (#1695)

* chore(deps): bump the prod-dependencies group with 8 updates

Bumps the prod-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [ical-generator](https://github.com/sebbo2002/ical-generator) | `10.2.0` | `11.0.0` |
| [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` |
| [tar](https://github.com/isaacs/node-tar) | `7.5.15` | `7.5.16` |
| [i18next](https://github.com/i18next/i18next) | `26.2.0` | `26.3.1` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` |
| [react-konva](https://github.com/konvajs/react-konva) | `19.2.4` | `19.2.5` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.15.1` | `7.17.0` |


Updates `ical-generator` from 10.2.0 to 11.0.0
- [Release notes](https://github.com/sebbo2002/ical-generator/releases)
- [Changelog](https://github.com/sebbo2002/ical-generator/blob/develop/CHANGELOG.md)
- [Commits](sebbo2002/ical-generator@v10.2.0...v11.0.0)

Updates `sharp` from 0.34.5 to 0.35.1
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](lovell/sharp@v0.34.5...v0.35.1)

Updates `tar` from 7.5.15 to 7.5.16
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](isaacs/node-tar@v7.5.15...v7.5.16)

Updates `i18next` from 26.2.0 to 26.3.1
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](i18next/i18next@v26.2.0...v26.3.1)

Updates `react` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `react-konva` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/konvajs/react-konva/releases)
- [Commits](https://github.com/konvajs/react-konva/commits)

Updates `react-router-dom` from 7.15.1 to 7.17.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/[email protected]/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: ical-generator
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: prod-dependencies
- dependency-name: sharp
  dependency-version: 0.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: tar
  dependency-version: 7.5.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: i18next
  dependency-version: 26.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: react-konva
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>

* fix(deps): repair react override mismatch in prod-dependencies bump

Dependabot's prod-dependencies bump updated the react/react-dom edges in
client/ and docs/ to 19.2.7 but left the root package.json `overrides`
pinned at 19.2.6. The override forced the top-level react node to 19.2.6
while workspaces requested 19.2.7, leaving duplicate React copies in the
tree. This crashed the client bundle at runtime ("Cannot read properties
of null (reading 'useRef')") and failed all E2E, and corrupted the tree
such that `npm audit signatures` mis-resolved the @docusaurus/react-loadable
alias (ETARGET [email protected]) — failing Static Analysis.

Bump the overrides to 19.2.7 to match the workspace edges and regenerate
the lockfile with a clean `npm install`. react and react-dom now dedupe to
a single 19.2.7 across all workspaces.

Co-Authored-By: Claude backend-developer (Haiku 4.5) <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude backend-developer (Haiku 4.5) <[email protected]>
…tTests (#1697)

- Add `workerGracefulExitTimeout: 2000` to the top-level Jest config; the
  500 ms default is too tight for jsdom/file-watcher cleanup in the
  resource-constrained sandbox and produces spurious force-kill warnings.
- Add `test:collect` and `test:collect:json` npm scripts using Jest 30.4.0's
  `--collectTests` flag to enumerate all test suites and names without
  executing them.
- Document `test:collect` in CLAUDE.md Common Commands table.

Fixes #1573

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
…) (#1698)

OrientationsTab referenced undefined CSS module classes so orientation
rows rendered completely unstyled. Mapped all class references to the
existing defined classes used by TradesTab and other Manage tabs
(itemsList/itemRow/itemInfo/itemDetails/itemSortOrder/itemActions/
editActions/saveButton) and restructured view-mode JSX to match
TradesTab layout.

Added 20 unit tests covering both view and edit modes of OrientationsTab,
and a deep-link E2E test verifying the Orientations tab is selected when
navigating directly to #orientations.

Fixes #1687

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude dev-team-lead (Sonnet 4.6) <[email protected]>
steilerDev and others added 20 commits June 15, 2026 21:48
…-up (#1591) (#1699)

The variance computation in AutoItemizePage sums effectiveLineAmount() which
grosses up net (includesVat:false) amounts by 1.19. THREE_LINES has all three
lines as net: 900+680+120 net = 1071+809.20+142.80 = 2023 gross. The test was
using invoice amount=1700 (raw net sum), causing an immediate ~19% variance on
page load and deterministic varianceMatch assertion failure.

Fix: set invoice amount=2023 (gross total) so the initial state is a genuine
0% match, then editing line 0 to 500 net (=595 gross) produces a new gross
total of 1547 vs 2023 = ~23.5% > 5% = varianceDanger as expected.

Co-authored-by: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>
…ion (#1702)

* fix(llm): disable model thinking/reasoning for budget invoice extraction

Gemini 2.5 Flash has dynamic thinking enabled by default on its
OpenAI-compat endpoint, causing 30s+ timeouts that surfaced as
LLM_UNREACHABLE errors during budget invoice extraction.

Fix: add per-provider reasoning suppression in buildRequestBody():
- Gemini: always send reasoning_effort:"none" (disables dynamic thinking)
- Ollama: always send reasoning_effort:"none" (safely ignored on
  non-thinking models)
- OpenAI: send reasoning_effort:"none" ONLY for reasoning models
  (o1/o3/o4/gpt-5 series, codex-mini); non-reasoning models such as
  gpt-4o reject the field with HTTP 400, so an isOpenAiReasoningModel()
  helper guards the injection with conservative pattern matching
- Anthropic: no change (thinking already disabled by default)
- Generic: no change (conservative — avoid breaking unknown providers)

Fixes #1701

Co-Authored-By: Claude backend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <[email protected]>

* fix(budgetExtraction): change OpenAI reasoning effort from 'none' to 'low'

OpenAI reasoning models (o1/o3/gpt-5 series) do not support reasoning_effort='none' —
they only accept 'low|medium|high'. The 'none' value caused HTTP 400 errors.

Using 'low' is valid across all OpenAI reasoning model families and still minimizes
reasoning cost for structured extraction. Gemini and Ollama continue to use 'none'
(which they support) to fully disable thinking.

Fixes #1701

Co-Authored-By: Claude backend-developer (Haiku 4.5) <[email protected]>

* test(budgetExtraction): update OpenAI reasoning_effort expectation to 'low'

Update o3-mini and gpt-5 test expectations to reflect that OpenAI
reasoning models require 'low' (not 'none') as the minimum reasoning
effort value — matching the production fix in f53a932.

Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude backend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude backend-developer (Haiku 4.5) <[email protected]>
…1707)

Add maximum-scale=1 and user-scalable=no to the viewport meta tag so iOS Safari no longer auto-zooms when an input is focused and pinch/double-tap zoom is disabled, preserving the native-app feel on mobile.

Fixes #1707

Co-Authored-By: Claude frontend-developer (Haiku) <[email protected]>
…ll (#1712)

Track the SearchPicker dropdown to its input field during mobile scroll via a requestAnimationFrame loop, with a close-on-out-of-view guard and an equality guard to avoid per-frame re-renders. Portal + position:fixed anti-clipping (#1601) preserved.

Fixes #1708

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>
…view and fix silent create failure (#1703, #1704)

Extract shared auto-itemize components (line card, line list, budget-line picker modal) consumed by both the new-invoice and existing-invoice flows. Give the new-invoice page the two-column side-by-side PDF review layout and surface previously-silent create errors (inline vendor field error + visible banner).

Fixes #1703
Fixes #1704
… annotation (#1705)

* fix(photo-annotator): responsive scaling and touch support for mobile annotation

Fix photo annotation editor to be responsive on mobile/tablet and enable touch
drawing. The Stage is now fit-scaled within its container using ResizeObserver,
keeping all shape coordinates in intrinsic photo-pixel space (no serialisation
format changes). Stage width/height are scaled by fitScale, Stage.scaleX/Y are
set to fitScale, and KonvaImage uses intrinsic dimensions. All drawing tools
now use pointer events (onPointerDown/Move/Up) instead of mouse events. Pointer
capture is enabled for reliable multi-touch drawing. The 1.0 fitScale cap
prevents excessive zoom on small photos. Inline text input positioning already
correctly computes scale from stageRect.width / photo.width, which equals
fitScale.

Changes:
- konvaInit.ts: Add Konva.hitOnDragEnabled = true for better touch support
- PhotoAnnotator.module.css: Add touch-action: none to .canvasArea
- PhotoAnnotator.tsx:
  * Add canvasAreaRef and containerSize state
  * Add ResizeObserver effect to measure container dimensions
  * Add pointer capture effect for multi-touch drawing
  * Compute fitScale = min(containerW/photoW, containerH/photoH, 1.0)
  * Replace stageWidth/Height with scaled values
  * Keep KonvaImage dimensions as intrinsic (not scaled)
  * Rename all Stage handlers: onMouseDown/Up/Move → onPointerDown/Up/Move
  * Add scaleX={fitScale}, scaleY={fitScale} to Stage
  * Add ref={canvasAreaRef} to canvas container div

Fixes #1705

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>

* fix(photo-annotator): add unit/E2E tests and fix ResizeObserver deps

Add comprehensive unit tests for PhotoAnnotator covering responsive
scaling, touch support, and annotation CRUD on mobile viewports.
Extend react-konva Jest manual mock to support shape refs/methods
used by the annotator. Add E2E scenarios 24-26 validating annotation
creation on tablet/mobile, pinch-to-zoom interaction, and annotation
persistence across device breakpoints.

Also fix ResizeObserver effect dependency array ([] → [imageLoaded])
so the canvas re-sizes correctly when the image finishes loading.

Fixes #1705

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>

* fix(photo-annotator): support mouse+touch drawing and fit-to-container scaling

Binds both mouse (onMouseDown/Move/Up) and touch (onTouchStart/Move/End)
events to the same shared handlers on the Konva Stage, so desktop mouse
drawing and mobile touch drawing both work through a single code path.
Handlers accept MouseEvent | TouchEvent and call preventDefault() on touch
events to suppress scroll interference. The pointer-capture effect (which
only fires on mouse) is removed, eliminating the regression that broke
desktop drawing after the previous responsive-scaling commit.

Unit tests updated to assert both mouse and touch handler props are
forwarded; stale pointer-capture test removed. react-konva mock updated
to forward mouse/touch handler-presence flags and includes the earlier
ESM fix (no jest.fn() at module scope).

Fixes #1705

Co-Authored-By: Claude frontend-developer (Haiku) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet) <[email protected]>

* fix(photo-annotator): use relative pointer position so shapes map to intrinsic coords

Drawing handlers previously called `getPointerPosition()` which returns
coordinates in Stage/screen space. When `fitScale < 1` (large photos or
small viewports), the Stage is scaled down, so screen-space coords are
compressed relative to the intrinsic image dimensions. Shapes therefore
landed in the top-left quadrant instead of where the user drew.

Switch to `getRelativePointerPosition()` on the layer, which automatically
applies the inverse of the Stage transform and returns coordinates in
intrinsic image space — matching the coordinate system used when the
annotation is rendered back over the full-resolution image.

New unit test asserts the round-trip under fitScale=0.5: a pointer event
at screen (100,80) must produce a shape at intrinsic (200,160). The
react-konva mock now exposes `getRelativePointerPosition()` as a distinct
method (returning x*2, y*2 for the 0.5 scale fixture) separate from
`getPointerPosition()`.

Fixes #1705

Co-Authored-By: Claude frontend-developer (Haiku) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet) <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude frontend-developer (Haiku 4.5) <[email protected]>
Cherry-picks the #1706 mobile lightbox metadata-toggle fix onto beta (originally merged to main as v2.7.1, leaving beta behind). Reconciles beta with main.

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>
…1714)

Adopt @floating-ui/[email protected] (ADR-033) for SearchPicker dropdown positioning, replacing the hand-rolled portal/getBoundingClientRect/rAF approach. useFloating fixed-strategy + offset/flip/shift/size + autoUpdate + FloatingPortal keeps the dropdown anchored to its input during mobile momentum scroll and soft-keyboard/visualViewport shifts — the cases the prior rAF fix (#1712) could not handle. FloatingPortal preserves the #1601 anti-clipping; an isPositioned gate avoids the first-frame flash. Drops the hide() middleware (it blanked the dropdown inside modals).

Fixes #1708

Co-Authored-By: Claude frontend-developer (Haiku 4.5) <[email protected]>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <[email protected]>
Co-Authored-By: Claude product-architect (Sonnet 4.5) <[email protected]>
chore: reconcile main into beta (recover from mis-targeted #1710 squash)
Adds a required-status-check workflow that fails any pull request targeting
'main' whose head branch is not 'beta'. Mark the 'guard' job as a required
check on the main ruleset to block out-of-band promotions (the #1710 class of
incident, where a beta-based fix branch was squash-merged directly into main).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
ci: enforce that PRs into main originate from beta
…in CI (#1718)

* fix(e2e): harden Scenario 14 against slow getDiaryEntry response in CI

Register a page.waitForResponse for GET /api/diary-entries/:id BEFORE
clicking the draft card, then await it after waitForURL completes. This
ensures the heading/draftBadge assertions only fire after the API
response has arrived, not after an arbitrary 15s wall-clock timeout.

Also align both toBeVisible() timeouts to 45_000 (matching test.slow()'s
3× budget) so CI load spikes cannot exhaust the deadline.

The previous { timeout: 15_000 } overrides bypassed test.slow()'s
tripled expect.timeout and caused intermittent failures when the server
response exceeded 15s under 8-worker CI load.

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.6) <[email protected]>

* fix(ci): install chrome-headless-shell for Playwright 1.60 and bump browser cache key

Since Playwright 1.49+, `chrome-headless-shell` is a SEPARATE browser binary
from `chromium` and must be explicitly installed. The previous install command
(`npx playwright install chromium webkit`) omitted it, so every E2E test that
uses the headless-shell channel failed with:

  browserType.launch: Executable doesn't exist at
  .../chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell

Fix: add `chromium-headless-shell` to the warmup install step.

The browser cache key is bumped from `playwright-v3` to `playwright-v4` (all
four restore/save references) so any stale cached browser set that is missing
the headless-shell binary is bypassed and a correct, complete set is re-seeded.
The apt system-deps cache key (`apt-v3-playwright-*`) is intentionally left
unchanged — only the `~/.cache/ms-playwright` browser cache key changes.

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.6) <[email protected]>

* fix(ci): add Playwright browser install-on-cache-miss fallback to smoke and shard jobs

The E2E Cache Warmup job installs browsers and saves them under the
`playwright-v4-{version}-{os}` cache key. However, the saved cache is
not reliably visible to downstream jobs (e2e-smoke, e2e) — they receive
"Cache not found for input keys: playwright-v4-..." and subsequently
fail with "browserType.launch: Executable doesn't exist at
.../chromium_headless_shell-1223/...".

Changes:
- Add `id: browser-cache` to the "Restore browser cache" step in both
  the smoke job and the shard matrix job (was already present in the
  warmup job).
- After the "Install Playwright system dependencies" step in each of
  those two jobs, add a new conditional step that runs
  `npx playwright install chromium chromium-headless-shell webkit` only
  when `steps.browser-cache.outputs.cache-hit != 'true'`.

This guarantees browsers are present regardless of cache visibility,
eliminating the "Executable doesn't exist" failure class that causes
entire shards to fail when the warmup cache is not populated or not
accessible on the runner.

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.6) <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude e2e-test-engineer (Sonnet 4.6) <[email protected]>
* docs: update documentation for v2.8.0 release

Document Paperless-first invoice creation with auto-itemize, mobile-first
photo capture with orientation tagging, diary daily-log vendor/work-hours,
and the relocated document unlink action. Refresh README, RELEASE_SUMMARY,
and add a new Capturing Photos guide.

Co-Authored-By: Claude docs-writer (Sonnet 4.6) <[email protected]>

* chore: add E2E test.slow() timeout-override lesson to implementation checklist

Captures the v2.8.0 release lesson: test.slow() triples the project
expect.timeout but an explicit per-assertion { timeout } override negates
the tripling, causing intermittent CI failures (diary Scenario 14).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude docs-writer (Sonnet 4.6) <[email protected]>
* chore: retrigger promotion CI (post-docs auto-fix skip-ci)

The auto-fix bot pushed a [skip ci] commit to beta after the docs merge,
advancing the v2.8.0 promotion PR #1700 HEAD to a commit with no CI runs.
This clean-titled commit advances beta with a real (markdown-only) diff so
CI fires and pull_request:synchronize re-runs on #1700.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* chore: nudge promotion CI synchronize

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Frank Steiler <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
@steilerDev steilerDev mentioned this pull request Jun 16, 2026
7 tasks
@steilerDev steilerDev merged commit 3edc4d4 into main Jun 16, 2026
59 of 65 checks passed
@steilerDev steilerDev deleted the promote/v2.8.0 branch June 16, 2026 18:03
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.8.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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant