Commit Graph

3207 Commits

Author SHA1 Message Date
Marcel
ad067d2e0e refactor(notification): provide notification store via context + fixture
Converts the module-singleton notificationStore into a context-provided
store so its specs can drive it without mocking the module. notifications.svelte
now exports createNotificationStore() (the former singleton body), plus
provideNotificationStore()/getNotificationStore()/NOTIFICATION_KEY mirroring
the confirm service. Root +layout provides it; NotificationBell and the
Chronik page read it via getNotificationStore().

Tests:
- notifications.svelte.spec drives a fresh createNotificationStore() per test
  (replacing __resetForTest/__setNavigateForTest with setNavigate()).
- notification.test-fixture.svelte wraps the bell, provides the store, and
  exposes setNotifications(items) via onReady (option b).
- NotificationBell.svelte.spec asserts the announced unread count across the
  empty / single / many / error a11y states (AC#5), stubbing EventSource+fetch.
- aktivitaeten page spec injects a real store via render context.

Per the recorded Phase-2b decision (full context refactor). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
29015ee864 test: inject real ConfirmService via context (batch 2/2)
Completes Phase 2a: geschichten/[id], persons/[id]/edit and admin/tags/[id]
page specs now provide a real createConfirmService() via render context
instead of mocking confirm.svelte. Zero confirm.svelte vi.mocks remain
across the client suite (AC#4). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
b1b8505b93 test: inject real ConfirmService via context (batch 1/2)
Replaces the vi.mock('$lib/shared/services/confirm.svelte') stub with a
real createConfirmService() provided through render's context map, mirroring
the existing admin/tags/[id]/page.svelte.spec.ts pattern. The generic
confirm.test-fixture.svelte renders only ConfirmDialog and cannot wrap an
arbitrary page; none of these specs trigger confirm(), so the children's
getConfirmService() simply reads the provided context instead of a module
mock. No vi.mock of confirm.svelte remains in these 5 specs. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
abe860bec7 test(hooks): migrate useUnsavedWarning spec to shared $app/navigation mock
Replaces the local beforeNavigate-capture plumbing and simulateNavigate
helper with the shared $mocks/$app/navigation module via a sync factory.
The per-test reset now comes from the shared module's embedded beforeEach.
Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
ec9d46da7a test(mocks): add shared $app/navigation mock with simulateNavigate
Exports the standard nav functions as vi.fn() and a beforeNavigate that
captures the registered callback. The exported simulateNavigate(href)
helper fires that callback and returns the cancel spy — the whole
capture-and-fire pattern lives in the shared module, not the raw callback.
An embedded beforeEach clears the captured callback and the mock call
histories before every test. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
e562b3bbea test: migrate remaining 3 $app/forms consumers to shared mock
Completes Phase 1a after the load-bearing ChronikFuerDichBox spec proved
the pattern. ChronikFuerDichBox.test and NotificationDropdown.test (rich
result-firing interceptors) keep their submit-fired assertions
(optimisticMarkRead/MarkAllRead) and use formsMock.setFormResult for the
failure branch. NotificationBell.spec used the simpler intercept-only
factory and renders no form of its own, so it adopts the shared superset
purely as a render-time stub. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
e725910402 test(activity): migrate ChronikFuerDichBox spec to shared $app/forms mock
Load-bearing first migration (ADR-012): this is the hardest case — its
enhance submit callback actually fires and reads the form result. Replaces
the duplicated 23-line interceptor factory with vi.mock('$app/forms',
() => ({ ...formsMock })) via $mocks, and the per-test mockFormResult
mutation with formsMock.setFormResult({ type: 'failure' }). The reset now
comes from the shared module's embedded beforeEach. The existing
optimisticMarkRead/optimisticMarkAllRead-on-submit assertions remain as the
positive proof the callback fired. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
782a34e34b test(mocks): add shared $app/forms interceptor mock body
Single home for the non-trivial form-interceptor enhance() shared by the
four complex consumers: it intercepts submit, invokes the SubmitFunction,
and fires the returned callback with a configurable result. setFormResult()
drives the success/failure branch; an embedded beforeEach resets it before
every test so isolation is structural. Consumed via vi.mock('$app/forms',
() => ({ ...formsMock })) through the $mocks alias. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
30f450b0d1 build(frontend): register $mocks in kit.alias for tsconfig resolution
The vite resolve.alias (added for the client + coverage runs) does not
reach svelte-check, which resolves paths through the generated tsconfig.
Declaring $mocks in kit.alias feeds both the generated tsconfig paths and
the sveltekit() vite plugin, so editor/type-check resolve it too. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
d4c0287e92 docs(adr): amend ADR-012 with no-factory ban + shared-mock dedup (#560)
Records the 2026-06-02 revision from #560: (1) no-factory vi.mock of a
SvelteKit virtual module is forbidden (the PR #657 partial-mock failure),
guarded by a seventh enforcement layer; (2) shared mock body + per-spec
sync factory via the $mocks alias is the sanctioned dedup; (3) Option C
config-level auto-resolve is rejected. Also corrects the stale 4.1.0
patch filename to 4.1.6 and links #657. Part of #560.
2026-06-03 11:38:22 +02:00
Marcel
301cfc5c9e test(meta): ban no-factory vi.mock of virtual modules
A vi.mock('$app/navigation') with no factory does not auto-resolve to a
__mocks__ file for SvelteKit virtual modules — it substitutes some
exports and leaves others (replaceState) bound to the live router, which
is exactly the PR #657 failure. This Node-mode source scan, mirroring
no-async-mock-factories and no-duplicate-mock-ids, fails at every vitest
invocation if any *.svelte.{spec,test}.ts reintroduces the pattern, and
forecloses ADR-012's rejected Option C. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
724c3881e4 build(frontend): add $mocks alias for shared browser-test mock bodies
Declares $mocks -> src/__mocks__ in both vite.config.ts and
vitest.client-coverage.config.ts so shared mock modules resolve in the
client test run and the coverage job alike. Enables the sync-factory
dedup pattern from ADR-012 (vi.mock('$app/forms', () => ({ ...formsMock }))).
Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
fab2930ca8 build(frontend): exact-pin @vitest/browser-playwright to 4.1.6
Drop the caret so the version cannot float off the patched release.
patches/@vitest+browser-playwright+4.1.6.patch backports vitest PR #10267
(the duplicate-mock-id birpc race, ADR-012) and only applies to 4.1.6; a
caret range could resolve to a version the patch rejects. A top-level
"//" key records the removal condition since package.json forbids
comments. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
d83707ec3b refactor(admin-tags): migrate tag-edit page from $app/stores to $app/state
The legacy $app/stores subscription API is replaced with the modern
$app/state reactive proxy (page.url.pathname), per ADR-012's
architectural follow-on. The two spec mocks of $app/stores are replaced
with sync-factory $app/state mocks, matching the existing convention in
aktivitaeten/documents specs. Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00
Marcel
caea0d5633 test(persons): assert the card title by exact message, not regex
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m13s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m36s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m4s
toHaveAttribute compares by equality, so passing a regex asserted against
the literal RegExp object and failed. Assert the full title against
m.person_correspondents_search_title(...) instead — it names both persons
and avoids retyping the copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
2bf14aeab9 docs(e2e): fix stale spec listing after Briefwechsel removal
The e2e README still listed the deleted korrespondenz.spec.ts. Replace it
with the new briefwechsel-removed.spec.ts guard entry — closing the last
dangling reference flagged in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
5b565d5271 docs(adr): record the bilateral->unidirectional search regression (ADR-030)
Removing the Briefwechsel view retargets its one inbound link to document
search, which filters sender AND receiver — A->B only. The bidirectional
"replies" direction is intentionally dropped. ADR-030 records the
context, decision and consequences, and notes a bidirectional search
filter as the superseding future enhancement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
df0f4879b8 docs: remove Briefwechsel from architecture, routes and glossary
Drop the Briefwechsel route and the conversation derived-domain /
conversation-thread prose from the route tables (CLAUDE.md,
frontend/CLAUDE.md), ARCHITECTURE.md, the C4 frontend/backend diagrams,
and GLOSSARY.md (term + derived-domain list). Delete the two superseded
Briefwechsel design specs. Historical ADRs and dated analyses are left
untouched as point-in-time context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
98d081397e chore(api): regenerate TS client without the conversation endpoint
Drop the /api/documents/conversation path and its getConversation
operation from the generated client to match the removed backend
endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
4e68b81bf7 feat(document): remove conversation repository queries
Delete findConversation and findSinglePersonCorrespondence (no remaining
callers after the service methods were removed) and their integration
test section. Drops the now-unused LocalDate import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
985b31f71f feat(document): remove conversation service methods
Delete getConversationFiltered (the endpoint's only caller is gone) and
the dead 2-arg getConversation(personA, personB) which had zero callers,
along with both getConversationFiltered test blocks. The hasSender/
hasReceiver specifications stay — document search still uses them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
3fb312b1c6 feat(document): remove the conversation endpoint
Delete GET /api/documents/conversation and its controller handler — the
only client was the removed Briefwechsel view. Drops the now-unused Sort
import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
e2ec45f819 refactor(document): move ConversationThumbnail into lib/document
With the Briefwechsel view gone, lib/conversation/ held a single shared
component whose only consumer is lib/document/ThumbnailRow. Move it (and
its spec) into lib/document/, update the import, delete the now-empty
lib/conversation/ folder, and fix the stale frontend/CLAUDE.md lib map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
7d9526440a feat(i18n): remove orphaned conversation message keys
Drop the 22 message keys that only the deleted Briefwechsel view used
(conv_* except the still-used conv_sort_newest/oldest, plus
nav_conversations, doc_conversation_title and person_correspondents_hint,
all now superseded by the retargeted card's new search keys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
13bbfa7abd test(briefwechsel): guard the removed /briefwechsel route returns 404
Add an active e2e spec asserting /briefwechsel 404s on the styled app
error page. The old assertion lived in stammbaum.spec.ts inside a
test.skip() block (never executed) and asserted the opposite — remove it.
Drop /briefwechsel from the auth protected-route loop; /documents (the
redirect target) sits behind the same authenticated() rule, so coverage
is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
975223c972 feat(briefwechsel): remove the standalone Briefwechsel view and its tests
Delete the /briefwechsel route in full (page, server load, eight
components and all co-located unit tests) and its end-to-end coverage
(briefwechsel-rows.visual, briefwechsel-a11y, the bilateral-correspondence
fixture, and the stale korrespondenz spec which targeted the route's
former /korrespondenz path). The card link now deep-links into document
search, so this view has no remaining inbound references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
403a043d51 feat(persons): retarget frequent-correspondents card to document search
The "Häufige Korrespondenten" card linked into the standalone Briefwechsel
view. Retarget each chip to the existing document search pre-filtered by
sender and receiver (/documents?senderId=A&receiverId=B), naming both
persons in a search-action title, swapping the chat-bubble icon for a
magnifier, and clarifying that the ×N badge counts shared letters in both
directions (not the unidirectional search result count).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:26:54 +02:00
Marcel
e259908d6a fix(stammbaum): order keyboard tab stops by visual layout, not DB order (#718)
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m21s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m40s
CI / fail2ban Regex (push) Successful in 43s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
Person nodes rendered in `nodes` array order (backend/DB row order), so
Tab focus hopped between nodes unrelated to their on-screen position,
failing WCAG 2.4.3 Focus Order (Level A).

Render the node loop in reading order instead: sort by layout y (top
generation first) then x (left-to-right within a row), via a
`nodesInReadingOrder` derived. Nodes without a layout position sort last
(mirroring the `{#if pos}` guard); node.id is the final tie-break for a
total, deterministic comparator. Shift+Tab and reload-stability fall out
for free (reversed render order; x/y independent of backend order).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 07:55:47 +02:00
Marcel
7d37e610da test(frontend): exclude mentionNodeView from server coverage (#628)
Some checks failed
CI / fail2ban Regex (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
CI's node coverage run (vite.config.ts, 'measure utility + server-side logic
only') counts every .ts under the include globs via all-files, but the Tiptap
NodeView builds live ProseMirror DOM and only runs in the browser editor — it is
exercised by the client project's browser tests, not the node run. Left in, it
showed 0% and dragged global functions (78.68%) and branches (78.48%) below the
80% gate.

Exclude it alongside the .svelte / browser-only UI files this config already
measures around. Restores the gate: statements 88.82%, branches 82.3%,
functions 87.27%, lines 89.77% (server project, verified locally).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
9c1eb7608b fix(transcription): harden re-edit pencil hit-testing + disable sync (#628 review)
Addresses the clean-agent review of PR #717:

- C1: the hidden pencil was opacity-0 only, which still hit-tests; its 44px box
  overhangs adjacent text, so a click in the gap between two mentions could land
  on the invisible button and spuriously open the dropdown (AC-8 hole). Add
  pointer-events-none while hidden, re-enabled with the opacity reveal on
  hover/focus.
- C2/N1: editor.setEditable() emits "update", not a ProseMirror transaction, so
  the NodeView's 'transaction' listener missed a mid-session disable flip (stale
  aria-disabled/tabindex; the comment was wrong). Listen on 'update' instead —
  which also skips selection-only changes, so it fires far less often.
- N2: track the node across update() so the pencil opens with the live
  displayName (hardening; relink only swaps personId today).

Tests: structural guard that the hidden pencil is pointer-events-none + reveals,
and a mid-session disable-flip test (fixture gains an onReady setDisabled hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
9bba5e4a7a feat(transcription): announce re-edit context via the existing live region (#628)
Passes editingDisplayName into MentionDropdown; the persistent aria-live region
announces person_mention_editing_announce({displayName}) on re-edit open and
falls back to the prompt/empty/count copy once the user edits or results arrive.
Routed through the SAME sr-only region as the result count — no second live
region (avoids the double-announce bug Leonie S-2 fixed). Fresh-@ passes an
empty editingDisplayName, so its announcements are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
751a48b22c test(transcription): AC-7 disabled, AC-8 no-mention, security clip/provenance (#628)
- AC-7: disabled editor → pencil is disabled + aria-disabled + tabindex -1, and
  neither keyboard nor pointer activation mounts a dropdown (WCAG 2.1.1, not just
  pointer-events-none).
- AC-8: plain text shows no pencil/dropdown; two adjacent mentions each keep one
  pencil with no spurious gap pencil and no auto-open; a doc-start mention still
  renders its pencil.
- Security: an oversized stored displayName clips the search query to 100 chars
  while the preserved node text stays full-length; re-link sources personId
  solely from the picked Person (p-anna), never the reflected/clipped text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
58a30a6e2e test(transcription): AC-6 single-dropdown invariant + stale-fetch guard (#628)
Locks in the single-owner controller guarantees: pencil→pencil, fresh-@→pencil
and pencil→fresh-@ all leave exactly one dropdown open; the request-token bump
on open discards a superseded open's in-flight fetch (open A → open B → A
resolves, deterministic, no sleeps). Plus a #380 AC-1 regression guard that the
fresh-@ path still inserts the typed text as displayName after the controller
refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
2430092e43 feat(transcription): dismiss + keyboard-operate the re-edit dropdown (#628 AC-4/AC-9)
Adds a visible × dismiss control to MentionDropdown (shared by the fresh-@ and
re-edit paths) and, for the re-edit path which has no Tiptap suggestion plugin
to forward keys, focuses the search input on open and handles its own keyboard:
Escape dismisses (AC-4), Arrow/Enter reuse the exported selection logic so the
dropdown is navigable on its own (AC-9 parity with the fresh-@ dropdown).

Both close paths (Escape + ×) leave the mention node attrs + text byte-identical
(AC-4) — close() never touches the document. Controller wires ondismiss=close
(+refocus editor) and focusOnMount only for the re-edit open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
4a93543645 feat(transcription): re-edit @mention via a pencil affordance (#628)
Hosts each mention as a Tiptap NodeView (mentionNodeView.ts) that renders the
@displayName token (textContent — never innerHTML) plus a contenteditable=false
pencil button in a fixed-width slot, revealed on whole-token hover and keyboard
focus (instant opacity swap, no reflow). Activating the pencil (click or Enter/
Space) opens the single mention dropdown via the controller, anchored at the
token and pre-filled with the stored displayName.

commitRelink swaps ONLY personId in place via setNodeMarkup, sourcing the id
solely from the selected Person — the stored displayName is preserved by
construction (AC-3), even after the search input is edited (AC-5, the #380 AC-1
invariant). renderHTML/renderText stay for serialization + clipboard.

AC-1/AC-2/AC-3/AC-5 + serializer round-trip covered by browser tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
b453c13bae refactor(transcription): lift @mention dropdown lifecycle into a single controller
Pulls mountedDropdown / requestId / debouncedSearch / dropdownState ownership
out of Tiptap's suggestion.render() closure into one createMentionController().
render() becomes a thin adapter: onStart→open, onUpdate→update, onExit→close.

This is the single-owner structure #628 needs for the AC-6 single-dropdown
invariant — the upcoming pencil re-edit affordance opens via the same
controller.open() instead of racing the suggestion plugin over module state.
open() now also bumps the request token so an open-A→open-B sequence discards
A's in-flight fetch (preserved increment-on-open semantics). No behaviour
change for the fresh-@ path — existing browser suite is the regression guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
599c3977fb feat(i18n): add re-edit @mention keys (edit/editing-announce/dismiss)
Keys for the re-edit affordance landing in #628:
- person_mention_edit_label   — pencil button aria-label
- person_mention_editing_announce — aria-live editing context
- person_mention_dismiss_label — dropdown close button aria-label

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:55:28 +02:00
Marcel
03e2615fa7 ci(deploy): use ::error:: annotations for smoke-test failures
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 3m37s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
nightly / deploy-staging (push) Successful in 2m1s
Convert the two bare failure echoes (gateway detection, /actuator status) to
::error:: so Gitea renders them as CI log annotations, consistent with the rest
of the deploy steps. No behaviour change. Raised in review (Leonie).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:41:07 +02:00
Marcel
3db6a3bf8f ci(deploy): correct stale POSTGRES_HOST --env-file comment
obs.env documents POSTGRES_HOST but does not set a value, so obs-secrets.env
does not 'override' it — it is the only source. Reword the carried-over comment
to match reality. Raised in review (Tobias).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:40:52 +02:00
Marcel
0e06626eef ci(deploy): guard deploy-obs heredoc stays unquoted (#603)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
The unquoted <<EOF delimiter is load-bearing — under a composite action secrets
come from $VAR (env), not Gitea ${{ secrets }} substitution, so a re-quote to
<<'EOF' would write literal $VAR strings and the five-key non-empty guard would
not catch it. Adds a self-testing grep guard (matching the ci.yml 'Assert no X'
convention) so a future re-quote fails CI instead of shipping broken obs auth.
Raised in review (Felix, Sara, Tobias).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:38:36 +02:00
Marcel
a47564934d ci(deploy): harden deploy-obs config step with set -euo pipefail
A failed cp/mkdir in the deploy-configs step was previously swallowed (the step
had no set -e), so a broken config copy could still reach the validate step. The
five-key guard catches empty secrets but not a failed copy. -u also catches a
typo'd env var name. Raised in review (Sara, Tobias).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:37:56 +02:00
Marcel
02fb16a0bd docs(ci): document composite actions in ci-gitea.md
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m39s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
Adds a Composite actions section covering the checkout-first ordering rule, the
secrets-via-inputs + unquoted-heredoc constraint (with the five-key guard and
shell: bash requirement), and a step-by-step for adding an input. Notes that the
inline Reload Caddy example now lives in the reload-caddy action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:25:32 +02:00
Marcel
4757a174c9 docs(adr): add ADR-029 composite actions for cross-workflow deploy logic
Records the decision to extract the shared obs-deploy/reload-caddy/smoke-test
logic into three composite actions instead of a reusable workflow or shared
shell script. Numbered 029 (028 was taken by the pdf.js wasm ADR on main since
the issue was filed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:24:20 +02:00
Marcel
75293c6aa8 ci(deploy): extend Renovate privileged-digest watch to .gitea/actions
The reload-caddy pinned alpine digest moved out of the workflow files into a
composite action. Add .gitea/actions/** to the manual-review digest rule so the
digest stays watched and never silently goes stale (#603).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:23:56 +02:00
Marcel
4e9b13c0e4 ci(deploy): wire release.yml to composite deploy actions
Replaces the four inline obs steps with one uses: ./.gitea/actions/deploy-obs,
and the Caddy reload + smoke test with one uses: each (host
archiv.raddatz.cloud, postgres_host archiv-production-db-1, PROD_* secrets).
Removes all three '# Keep in sync with nightly.yml' comments — the shared
definition now enforces the invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:23:41 +02:00
Marcel
ad27c1f757 ci(deploy): wire nightly.yml to composite deploy actions
Replaces the four inline obs steps with one uses: ./.gitea/actions/deploy-obs,
and the Caddy reload + smoke test with one uses: each (host
staging.raddatz.cloud, postgres_host archiv-staging-db-1, STAGING_* secrets).
checkout@v4 stays the first step; the #526 /import mount guard stays inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:23:05 +02:00
Marcel
0e30e5c570 ci(deploy): extract deploy-obs composite action
Five required, no-default inputs (incl. grafana_db_password for the #651
read-only reader role). Four named run: blocks keep the four CI log sections:
deploy configs, validate, start, assert health.

Secrets map to env: and are written via an unquoted <<EOF heredoc ('$VAR'
expands at the shell layer; a quoted delimiter would write the literal var
name and config --quiet would pass anyway). A five-key non-empty guard runs
right after the write, and chmod 600 is the final operation so the file is
never world-readable. ADR-016 absolute paths and the two-file --env-file
ordering are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:21:28 +02:00
Marcel
a6a8552a48 ci(deploy): extract smoke-test composite action
Parameterises the public-surface smoke test by host (one required input,
mapped via env: HOST). Keeps the three checks verbatim — login reachable,
HSTS value pinned, Permissions-Policy present, /actuator -> 404 — plus the
/proc/net/route gateway-detection and RESOLVE-array rationale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:20:09 +02:00
Marcel
b0d28c1e0b ci(deploy): extract reload-caddy composite action
First composite action in the repo (establishes the convention). Lifts the
Caddy reload step verbatim from nightly.yml/release.yml — DooD privileged
sibling + nsenter to systemctl reload caddy, pinned alpine digest, reload
not restart. No inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:19:36 +02:00
Marcel
420c0e3e10 docs(adr): record pdf.js wasm same-origin serving + future-CSP constraint
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m21s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 3m18s
CI / OCR Service Tests (push) Successful in 21s
CI / Backend Unit Tests (push) Successful in 3m45s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m3s
nightly / deploy-staging (push) Successful in 2m14s
Promote the future-CSP constraint from an inline Caddyfile comment to a
durable ADR-028: serve the pdf.js wasm decoders same-origin (never a
CDN), any future CSP must allow 'wasm-unsafe-eval' + worker-src 'self'
blob:, and the build-time guard keeps the wasm shipping. Caddyfile now
points at the ADR.

Addresses re-review: Markus (constraint should be an ADR, not a comment).

Refs #708

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:17:41 +02:00