Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8ffa993
feat(diary): add optional vendor and work start/end time with compute…
steilerDev Jun 13, 2026
fc23eea
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 13, 2026
d4c08cf
fix(auto-itemize): add VAT to total for net (includesVat=false) line …
steilerDev Jun 15, 2026
8fa9e49
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
12b4a2a
chore(lint): resolve @typescript-eslint/consistent-type-imports error…
steilerDev Jun 15, 2026
0b8250b
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
43865d6
chore(deps): align root and client Konva versions to eliminate type-c…
steilerDev Jun 15, 2026
d39cb80
fix(documents): move unlink action to discoverable top-right card ove…
steilerDev Jun 15, 2026
bcd5ec7
fix(diary): anchor mobile filter panel to bar so filters and search a…
steilerDev Jun 15, 2026
605e11b
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
0bac54d
chore(client): resolve set-state-in-effect and exhaustive-deps lint w…
steilerDev Jun 15, 2026
bcb3821
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
d111db7
chore(test-infra): audit Jest unit test memory + fix useNavigate anti…
steilerDev Jun 15, 2026
288ff46
chore(types): eliminate @typescript-eslint/no-explicit-any warnings (…
steilerDev Jun 15, 2026
61c7502
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
ae5c5a2
chore: zero out remaining ESLint warnings and errors (#1455) (#1692)
steilerDev Jun 15, 2026
cf2e594
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
5f072cd
chore(client): clear residual eslint warnings (web-api-no-leaked-fetc…
steilerDev Jun 15, 2026
608f304
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
6dc961c
feat(photos): mobile-first photo capture flow with metadata modal and…
steilerDev Jun 15, 2026
d1d2033
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
f27962e
feat(invoices): Paperless-first invoice creation with LLM auto-itemiz…
steilerDev Jun 15, 2026
b63fcfa
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
f1b8171
chore(deps): bump github/codeql-action in the github-actions group (#…
dependabot[bot] Jun 15, 2026
40a45cd
chore(deps-dev): bump the dev-dependencies group with 7 updates (#1694)
dependabot[bot] Jun 15, 2026
9b21257
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
bb92bcb
chore(deps): bump the prod-dependencies group across 1 directory with…
dependabot[bot] Jun 15, 2026
7e53bfe
chore(test): adopt Jest 30.4.0 workerGracefulExitTimeout and --collec…
steilerDev Jun 15, 2026
3aff01c
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
65f70e8
fix(manage): align OrientationsTab to defined CSS module classes (#16…
steilerDev Jun 15, 2026
b51708d
fix(e2e): correct Scenario 25 invoice amount to account for VAT gross…
steilerDev Jun 15, 2026
c67dfc1
fix(llm): disable model thinking/reasoning for budget invoice extract…
steilerDev Jun 15, 2026
7840409
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 15, 2026
97f0284
fix(client): disable mobile viewport zoom on input focus and pinch (#…
steilerDev Jun 16, 2026
2b126c2
fix(search-picker): anchor dropdown to input field during mobile scro…
steilerDev Jun 16, 2026
9dd4d21
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 16, 2026
6eea9b3
feat(budget): unify new-invoice auto-itemize UI with existing review …
steilerDev Jun 16, 2026
05e720d
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 16, 2026
5f7f6d4
fix(photo-annotator): responsive scaling and touch support for mobile…
steilerDev Jun 16, 2026
88748bb
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 16, 2026
759e14c
fix(photo-metadata): sync mobile lightbox toggle fix to beta (#1715)
steilerDev Jun 16, 2026
2dff282
fix(search-picker): replace hand-rolled positioning with Floating UI …
steilerDev Jun 16, 2026
0236b4d
Merge remote-tracking branch 'origin/main' into chore/sync-main-to-beta
Jun 16, 2026
45ed9c5
Merge pull request #1716 from steilerDev/chore/sync-main-to-beta
steilerDev Jun 16, 2026
2ac6ad5
ci: enforce that PRs into main originate from beta
Jun 16, 2026
ab67ad0
Merge pull request #1717 from steilerDev/ci/enforce-promotion-source
steilerDev Jun 16, 2026
1aa3937
fix(e2e): harden diary Scenario 14 + Playwright 1.60 browser install …
steilerDev Jun 16, 2026
f8c25c5
docs: update documentation for v2.8.0 release (#1719)
steilerDev Jun 16, 2026
370b180
style: auto-fix lint and format [skip ci]
steilerdev-cornerstone-bot[bot] Jun 16, 2026
9d4f3b7
chore: retrigger promotion CI (#1720)
steilerDev Jun 16, 2026
81d97bf
chore: nudge promotion CI via synchronize on pushable head
Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 155 additions & 1 deletion .claude/agent-memory/e2e-test-engineer/MEMORY.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions .claude/agent-memory/e2e-test-engineer/searchpicker-mobile-1708.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: searchpicker-mobile-1708
description: SearchPicker mobile anchor regression test details for Issue #1708 — rAF position-tracking fix
metadata:
type: project
---

# SearchPicker Mobile Anchor Regression (Issue #1708)

**Fix:** `SearchPicker.tsx` now runs a `requestAnimationFrame` loop while the dropdown is open to call `updateDropdownRect()` (syncs `position:fixed` dropdown with input's `getBoundingClientRect()`) and `closeIfOutOfView()` (closes if input scrolls off screen). Replaces the prior scroll/resize listeners which missed momentum-scroll frames on mobile WebKit.

**Test surface used:** `WorkItemCreate` page (`/project/work-items/new`) — AreaPicker (`SearchPicker<TreeNode>`) with `showItemsOnFocus={true}`.

**Why WorkItemCreate:** Easiest real-data surface. AreaPicker is on a full page (not a modal), uses `showItemsOnFocus=true` so no search text needed, and `createAreaViaApi` / `deleteAreaViaApi` are in `apiHelpers.ts`.

**Assertion pattern:**

```ts
const inputBottom = inputBox!.y + inputBox!.height;
const anchorDistance = Math.abs(dropdownBox!.y - inputBottom);
expect(anchorDistance).toBeLessThan(20);
```

Tolerance 20px accommodates: the 4px gap the component adds, sub-pixel rounding, and the flip-above path (when space below viewport < 308px, dropdown appears ABOVE input — both positions are adjacent, so `Math.abs` handles both).

**Mobile-only skip:** `viewportWidth > MOBILE_MAX_WIDTH (499)` → `test.skip()`. iPhone 13 width = 390px (passes). iPad (gen 7) = ~810px (skips on tablet project). Desktop = 1920px (skips).

**Why Scenario 2 is skipped:** The anti-clipping portal behavior (dropdown portals to `document.body`, bypassing `overflow:hidden` on the modal) is already unit-tested in `SearchPicker.test.tsx` → "dropdown is portalled to document.body". The candidate modal surfaces (MassMoveModal, InvoicePaperlessPickerModal, PhotoMetadataModal) all require complex setup that would slow the smoke suite without adding meaningful regression value.

**diary-list Scenario 9 failure (run 27579727461 shard 3):** `page.unroute: Target page, context or browser has been closed` — a cleanup-race where `page.unroute()` in a `finally` block fires after the page is already torn down. This is a pre-existing test hygiene issue in `diary-list.spec.ts`, not related to the current work.
18 changes: 18 additions & 0 deletions .claude/agent-memory/qa-integration-tester/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
> Detailed notes live in topic files. This index links to them.
> See: `budget-categories-story-142.md`, `e2e-pom-patterns.md`, `e2e-parallel-isolation.md`, `story-358-document-linking.md`, `story-360-document-a11y.md`, `story-epic08-e2e.md`, `story-509-manage-page.md`, `story-471-dashboard.md`

## Story #1705 — PhotoAnnotator responsive scaling + touch support tests (2026-06-16)

**react-konva Stage mock extended for forwardRef**: `__mocks__/react-konva.ts` now exports `Stage` as `React.forwardRef` with `useImperativeHandle` so `stageRef.current` is a mock Konva stage object. Exports: `stageMockContainer` (plain no-op fns at module scope; test installs jest.fn() spies in-place in beforeEach), `stageMockHandlers` (captured onMouseDown/Move/Up + onTouchStart/Move/End — NOT pointer events), `setMockStagePointerPosition(pos)`, `setMockStageRelativePointerPosition(pos)`. Import as `import * as ReactKonvaMock from 'react-konva'` (after `jest.mock('react-konva')`) for access.

**getPointerPosition vs getRelativePointerPosition discrimination (2026-06-16)**: The mock now exposes BOTH methods returning DIFFERENT independently-settable values. `getPointerPosition()` → `mockStagePointerPosition` (screen/container space). `getRelativePointerPosition()` → `mockStageRelativePointerPosition` (intrinsic image space). Test 6 in `#1705` describe block sets them to distinct values (screen: start=(50,50) end=(100,100); intrinsic: start=(100,100) end=(300,200)) then asserts the committed rectangle shape has x≈100, y≈100, w≈200, h≈100 (intrinsic coords). If production regressed to `getPointerPosition()` the shape would be x=50, y=50, w=50, h=50 — the `expect(rect.w).not.toBeCloseTo(50, 0)` assertion would catch it. The coordinate assertion reads `data-annotator-shapes` from `[role="application"]` which the production component keeps current via `data-annotator-shapes={JSON.stringify(undoStack.shapes)}`.

**#1705 revision 2 (2026-06-16) — pointer events removed, mouse+touch added**: Production code changed from `onPointerDown/Move/Up` to `onMouseDown/Move/Up` + `onTouchStart/Move/End`. The pointer-capture `useEffect` (which called `container.addEventListener('pointerdown', ...)`) was deleted. Test changes: (1) `HANDLER_PRESENCE_PROPS` in mock now includes `onTouchStart/Move/End` and keeps pointer handlers so absence is reported as 'false'; (2) `StageMockHandlers` captures mouse+touch, not pointer; (3) test 4 flipped — now asserts mouse+touch present, pointer absent; (4) test 5 (pointer-capture addEventListener/removeEventListener) deleted entirely; (5) test 7 renamed to 6, fires `onMouseDown/Move/Up` with `evt: new MouseEvent(...)` objects; (6) `beforeEach` no longer installs `addEventListener/removeEventListener/setPointerCapture` spies (only `getBoundingClientRect` remains).

**CRITICAL: jest.fn() MUST NOT appear anywhere in **mocks**/ files — not even inside exported functions**. In Jest ESM mode (`--experimental-vm-modules`), manual `__mocks__/` files for node_modules are auto-loaded (no `jest.mock()` call needed) by every test suite that imports the package. The jest global is NOT injected into mock modules — it is only available in test files. Any `jest.*` call in a mock file causes `ReferenceError: jest is not defined` in every suite that auto-loads the mock (including unrelated suites like DiaryEntryDetailPage). Pattern: export a plain object with no-op functions; the test's beforeEach mutates the object's properties with `jest.fn()` spies in-place. This avoids the constraint while keeping spy behavior available to test assertions.

**Stage DATA_FORWARDED_PROPS extended**: `width`, `height`, `scaleX`, `scaleY` now forwarded as `data-stage-width`, `data-stage-height`, `data-stage-scale-x`, `data-stage-scale-y`. Handler presence flags: `onPointerDown/Move/Up` and `onMouseDown/Move/Up` forwarded as `data-has-pointerdown`, `data-has-mousedown`, etc. (value 'true'/'false'). Stage element also gets `data-konva-stage-stub` in addition to `data-konva-stub`.

**ResizeObserver bug FIXED**: `useEffect` deps changed from `[]` to `[imageLoaded]`. Now attaches after `imageLoaded=true` when `canvasAreaRef.current` is the live canvasArea `<div>`. Tests 1, 3, 6 updated to assert CORRECT behavior: fitScale=0.5 (800×600 photo in 400×300 container), fitScale=0.1 (4000×3000 photo in 400×300 container), observe called 1×/disconnect called 1× on unmount.

**ResizeObserver mock fires synchronously in observe()**: The `makeResizeObserverMock` helper fires the callback immediately inside `observe()`. Combined with the 20ms `setTimeout` wait in `renderAnnotator`, the `setContainerSize` state update flushes and the Stage renders with correct scaled dimensions before assertions run. This pattern works reliably for testing fitScale behavior.

**pointer-capture effect test**: Uses `stageMockContainer.addEventListener.mock.calls` to check if 'pointerdown' was registered. Uses graceful fallback if stageRef not set (systemic worktree issue with useImperativeHandle deps=[]).

## Issue #1568 — Jest ESM mock static-import constraint (2026-06-15)

**jest.unstable_mockModule + static import before it = mock fails in CI**: In Jest 30 with `--experimental-vm-modules`, adding a static `import` statement BEFORE `jest.unstable_mockModule()` in a test file breaks mock registration for components that call the mocked module's code directly (e.g., `useFormatters()` → `useLocale()`). Components tested by files in shards 3/4 that also mock `LocaleContext` (or don't call `useFormatters()` directly) appear to pass — but that's because they have a safety net, not because the mock works. The inline factory pattern (all code inside the `jest.unstable_mockModule()` factory body, no imports before it) is REQUIRED for reliable mock registration in Jest ESM. Attempted shared-factory approach across 46 files was reverted.
Expand Down
5 changes: 4 additions & 1 deletion .claude/agent-memory/ux-designer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
- Tool palette: `role="toolbar"` wrapper; each button `.toolButton` / `.toolButtonActive`; `min-width/height: 44px`; `aria-pressed`; inline SVG icons (24×24, `stroke="currentColor"`); HighlightIcon uses `fill="currentColor"` (established precedent)
- Annotator dark-surface rgba values: `rgba(0,0,0,0.6)`, `rgba(255,255,255,0.4)` etc. in PhotoAnnotator.module.css are intentional photo-overlay hardcodes (pre-existing pattern); do NOT flag as token violations
- Font-size radiogroup: `role="radiogroup"` + `role="radio"` + `aria-checked`; buttons use `.fontSizeButton`/`.fontSizeButtonActive`; hover inside `prefers-reduced-motion` block (consistent with toolButton + strokeButton pattern)
- Inline text input (Story #1476): `.inlineTextInput` positioned absolute over canvas; focus managed via `requestAnimationFrame`; `aria-label` via `t('editText'|'editCallout')`; z-index should use `var(--z-dropdown)` not literal `10`; inline style should NOT duplicate CSS module's `min-width`/`z-index`
- Inline text input (Story #1476): `.inlineTextInput` positioned absolute over canvas; focus managed via `requestAnimationFrame`; `aria-label` via `t('editText'|'editCallout')`; `z-index: 1000` is pre-existing (should be `var(--z-modal)`, refinement item); inline style should NOT duplicate CSS module's `min-width`/`z-index`
- TextIcon uses SVG `<text>` element (not stroked path) — inconsistent with stroke icon family; flag for polish pass
- Annotation colors in `ANNOTATION_COLORS` are intentionally hardcoded hex (not tokens) — marks must be theme-invariant; document this in any spec touching that file
- Draft shape visual: `stroke-dasharray: 6 4`, `opacity: 0.8`, `pointer-events: none` — use for ALL new shape types
- Arrow committed: `<line>` + `<marker>` with `fill="context-stroke"` so one defs entry covers all colors; arrowhead on commit only (not during draft)
- Ellipse selection handles: 4 cardinal points (N/S/E/W) not 8; Arrow/Line: 2 endpoint handles
- `context-stroke` SVG2 fill on marker = no dark mode override needed for arrowhead
- Mobile: `.toolGroup` gets `width:100%` + bottom border at `<640px` via existing media query — no new CSS needed for new buttons
- Fit-to-container scaling (fix #1705): `fitScale = min(containerW/intrinsicW, containerH/intrinsicH, 1.0)`; Stage gets `width={intrinsicW*fitScale}` `height={intrinsicH*fitScale}` `scaleX/Y={fitScale}`; KonvaImage keeps `width={intrinsicW}` `height={intrinsicH}`; cap at 1.0 prevents upscaling small photos
- `touch-action: none` on `.canvasArea` and `.svgOverlay` only — correctly scoped to canvas area; does NOT affect scroll outside the annotator
- `sizeDropdownSelect:focus-visible` uses `outline: 2px solid var(--color-focus-ring)` (inconsistent with `box-shadow: var(--shadow-focus)` convention) — pre-existing refinement item

## Story #1478 — Photo Annotator A11y Audit

Expand Down
1 change: 1 addition & 0 deletions .claude/checklists/implementation-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ This checklist is updated after each epic's lessons-learned sync (see `/epic-clo
- [ ] **E2E text locators after label changes**: When a production PR renames a UI label, update all E2E test locators that match that text. Regex locators like `/hide linked/i` silently break when the label changes to "Hide already-linked documents" (no contiguous match). Prefer `data-testid` attributes for stability; when using text regex, keep the pattern broad enough to survive minor rewording (e.g. `/hide.*linked/i`).
- [ ] **E2E canvas interaction coordinates**: When interacting with a Konva `<canvas>` (or any element centered inside a flex container), use `page.locator('canvas').first().boundingBox()` — NOT the parent container's bounding box — to calculate mouse coordinates. A flex-centered canvas occupies only a portion of its parent; coordinates derived from the parent land outside the canvas and Konva's `getPointerPosition()` returns null, silently preventing shape commits.
- [ ] **E2E stale CI (E2E Cache Warmup cancelled)**: If a PR shows "E2E Cache Warmup cancelled" in CI (QG fails even though all test/lint/docker jobs pass), the branch is stale relative to beta. Rebase onto the latest `origin/beta` and force-push to trigger a fresh CI run.
- [ ] **E2E `test.slow()` vs explicit assertion timeouts**: `test.slow()` triples the project-level `expect.timeout` (e.g. 15s → 45s), but an explicit `{ timeout: 15_000 }` override on an individual `expect(...).toBeVisible()` _negates_ that tripling, capping the wait at the literal value. Under heavy parallel CI load this causes intermittent failures even though the app and API are correct. When a test calls `test.slow()`, either omit per-assertion timeout overrides (let the tripled budget apply) or set them to the full tripled value (e.g. `45_000`). Prefer awaiting the gating network response (`waitForResponse` registered _before_ the triggering click) over a fixed wall-clock timeout.

## i18n — Translations

Expand Down
22 changes: 17 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: playwright-v3-${{ steps.playwright-version.outputs.version }}-${{ runner.os }}
key: playwright-v4-${{ steps.playwright-version.outputs.version }}-${{ runner.os }}

- name: Fix apt cache ownership for runner
run: sudo chown -R $(id -u):$(id -g) /var/cache/apt/archives
Expand All @@ -296,15 +296,15 @@ jobs:
# --- Browser cache miss: install browsers and save ---
- name: Install Playwright browsers
if: steps.browser-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium webkit
run: npx playwright install chromium chromium-headless-shell webkit
working-directory: e2e

- name: Save browser cache
if: steps.browser-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: playwright-v3-${{ steps.playwright-version.outputs.version }}-${{ runner.os }}
key: playwright-v4-${{ steps.playwright-version.outputs.version }}-${{ runner.os }}

# --- Apt cache miss: download debs (without installing) and save ---
- name: Configure apt for download-only
Expand Down Expand Up @@ -398,10 +398,11 @@ jobs:
run: npm ci -w e2e

- name: Restore browser cache
id: browser-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: playwright-v3-${{ needs.e2e-warmup.outputs.playwright-version }}-${{ runner.os }}
key: playwright-v4-${{ needs.e2e-warmup.outputs.playwright-version }}-${{ runner.os }}

- name: Fix apt cache ownership for runner
run: sudo chown -R $(id -u):$(id -g) /var/cache/apt/archives
Expand All @@ -427,6 +428,11 @@ jobs:
exit 1
working-directory: e2e

- name: Install Playwright browsers (cache miss fallback)
if: steps.browser-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium chromium-headless-shell webkit
working-directory: e2e

- name: Run E2E smoke tests
run: npm run test:e2e:smoke

Expand Down Expand Up @@ -479,10 +485,11 @@ jobs:
run: npm ci -w e2e

- name: Restore browser cache
id: browser-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: playwright-v3-${{ needs.e2e-warmup.outputs.playwright-version }}-${{ runner.os }}
key: playwright-v4-${{ needs.e2e-warmup.outputs.playwright-version }}-${{ runner.os }}

- name: Fix apt cache ownership for runner
run: sudo chown -R $(id -u):$(id -g) /var/cache/apt/archives
Expand All @@ -508,6 +515,11 @@ jobs:
exit 1
working-directory: e2e

- name: Install Playwright browsers (cache miss fallback)
if: steps.browser-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium chromium-headless-shell webkit
working-directory: e2e

- name: Run E2E tests (shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
run: npm run test:e2e:shard

Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/enforce-promotion-source.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Enforce Promotion Source

# Guards the beta -> main promotion model: PRs targeting `main` must originate
# from `beta`. This prevents a feature/fix branch (typically based on beta) from
# being merged directly into main, which would drag in-flight beta work onto a
# stable release out-of-band (see the #1710 incident).
#
# Enforcement: mark the `guard` job as a required status check on the `main`
# ruleset. It only blocks at merge time; a non-beta PR into main stays blocked.

on:
pull_request:
branches: [main]

concurrency:
group: enforce-promotion-source-${{ github.ref }}
cancel-in-progress: true

jobs:
guard:
name: Require head branch == beta
runs-on: ubuntu-latest
steps:
- name: Verify PR originates from beta
if: github.head_ref != 'beta'
run: |
echo "::error::PRs into 'main' must originate from 'beta' (got '${{ github.head_ref }}'). Promote via a beta -> main PR instead."
exit 1
- name: Promotion source OK
if: github.head_ref == 'beta'
run: echo "Head branch is 'beta' — promotion source is valid."
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ Hand-written SQL files in `server/src/db/migrations/` with a numeric prefix (e.g
| `PHOTO_MAX_FILE_SIZE_MB` | `20` | Maximum photo upload size in MB |
| `PHOTO_STORAGE_PATH` | `{DB_DIR}/photos` | Directory for photo storage |
| `DIARY_AUTO_EVENTS` | `true` | Enable automatic diary event creation |
| `CURRENCY` | `EUR` | ISO 4217 currency code for formatting (exposed via `GET /api/config`) |
| `PAPERLESS_URL` | (none) | Paperless-ngx instance base URL |
| `PAPERLESS_API_TOKEN` | (none) | Paperless-ngx API authentication token |
| `PAPERLESS_EXTERNAL_URL` | (none) | Browser-facing URL for Paperless-ngx links (falls back to `PAPERLESS_URL` if unset) |
Expand Down
Loading
Loading