Commit Graph

108 Commits

Author SHA1 Message Date
5c03da2ccc #305: a full pond archive before deletion and before purge
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m16s
CI / Build container images (pull_request) Successful in 4m9s
CI / Auth e2e pack (pull_request) Successful in 9m19s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
Deleting a pond already had a strict prompt — typing the pond name, stricter
than a confirm dialog. That was never the gap. The gap is that the person who
deletes it loses access the moment they do: the pond leaves their view, only a
Site Admin can bring it back, and the export is no longer reachable for them.
So the archive is offered INSIDE the deletion flow, before the button.

What it contains, and why it is not the existing export:

- Every page the requester may read, as Markdown, as before.
- **Every attachment of the pond**, not only the embedded ones. An
  attachment nobody put on a page would otherwise vanish unnoticed — which
  is the whole reason this issue exists.
- `manifest.json`: pond settings (EFFECTIVE, defaults filled in — a
  preservation format must not require its reader to know Dorfteich's
  defaults), labels, the page hierarchy and sort keys, comments, and
  attachment metadata including the #199 hash so a reader can verify bytes.
  It extends the #210 manifest rather than adding a second descriptor, and
  carries an explicit `formatVersion`.
- `README.txt`, because the manifest is for machines: whoever unpacks a
  folder of Markdown a year from now must not believe they hold a one-click
  restore.

Decisions worth naming:

- **"Complete" describes the RESULT, not the route.** A pond admin who may
  read every page gets `complete: true`; only an archive that actually
  leaves pages out is incomplete. The Site-Admin route skips the read filter
  (an archive taken before an irreversible purge must not depend on which
  ponds the operator happens to be a member of) — those are two different
  questions and the first version of this conflated them.
- **The omission is named before the download**, with its number, in the UI
  and in the manifest. An archive silently missing content is worse than no
  archive, because it ends the search.
- **Not downloading stays allowed.** A pond of test pages should not require
  one, and the server cannot tell whether a file arrived anyway — so the
  finality is stated in text instead of enforced.
- **A plain link, not fetch-into-a-blob.** The api streams the ZIP; buffering
  a whole pond in the tab to draw a progress bar would trade memory for
  cosmetics. The browser reports progress and completion; what it cannot say
  — that the archive is being BUILT — is announced in a live region.
- Read trail unchanged in kind (ADR 0023): one `export` event per classified
  page before any classified byte enters the stream. Attachments never travel
  without their page, so the same events cover them.
- New audit action `pond.archived` (catalogue v1.7) with page and attachment
  counts, omitted pages, and completeness.

Format documented in `docs/architecture/pond-archive-format.md`, including
what is deliberately NOT in it (history, permissions, trash).

Verified by hand, not only asserted: a real pond's archive downloaded and
unpacked — README, manifest, three page files, the media file; the manifest's
effective settings, per-page classification, the VS-NfD frontmatter and
marking preserved in the classified page's Markdown, and the attachment's
sha256 present. Plus six api tests (including that an unembedded attachment
travels and that a Site Admin gets a complete archive without membership) and
the a11y pack 11/11 in both schemes, which now also scans the pond settings
screen.

Not done, because there is nothing to attach it to: the Site Admin's purge
dialog (#193) exists only as an api endpoint — there is no pond-trash UI in
the web app. The api half is here and tested, so it becomes a link when that
screen is built.
2026-08-01 20:04:56 +02:00
6377faf332 #306: instance branding — logo and favicon, cropped in the browser
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m28s
CI / Build container images (pull_request) Successful in 2m7s
CI / Auth e2e pack (pull_request) Successful in 9m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m47s
CD / Promote to Int (push) Successful in 16s
CI / Lint, typecheck, test (push) Successful in 7m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m41s
CI / Import/export fidelity gate (push) Successful in 1m12s
An instance had no way to look like itself: the top bar said "Dorfteich"
whatever the operator called their instance, `instance.name` was never
rendered in the running app at all, and there was no favicon anywhere —
`index.html` had no `<link rel="icon">` and `public/` held only fonts and
theme-init.js.

Where the line is drawn, and why:

- **The api never decodes an image.** Cropping, scaling and the conversion
  to PNG happen on a canvas in the browser; the api checks the PNG
  signature, reads the IHDR dimensions at their fixed offsets and enforces
  the caps. An image library would put a decoder in front of
  attacker-supplied bytes AND would have to be carried through the
  `--network none` offline build. Reading two big-endian integers is not
  decoding.
- **SVG is refused**, with its own error message rather than a generic
  "not a PNG": it can carry script, and serving it from our own origin
  would be a cross-site-scripting vector. An operator who tried one should
  learn that it is deliberate.
- **The crop is driven by number inputs, not by dragging.** A drag-only
  cropper excludes keyboard and switch users outright; a number input is
  arrow-key operable and screen-reader readable without any custom aria.
  The resulting pixel size is stated in text, not only drawn as a frame.
- **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has
  already resolved `data-theme` before first paint, so the correct logo is
  the one painted rather than the one that appears after a flash. Without a
  dark variant the LIGHT logo carries both themes — the operator's own
  asset shown unchanged beats one they did not choose (the rule #307
  extends to ponds). The settings screen warns; it never blocks.
- **The favicon link is static, its resource dynamic.** index.html stays a
  static file and the api answers with the uploaded icon or a shipped
  default — that route must never 404, or the browser keeps its generic
  icon for good. The default is generated by a script from Node's own zlib
  (`gen-default-favicon.mjs`), for the same offline-build reason.
- Both favicon sizes are uploaded together: one source, one crop, so the
  tab icon and the home-screen icon can never disagree.
- Branding is served WITHOUT a session, because the login screen carries it
  and the browser fetches the favicon before anyone signs in. The admin
  screen says so — an operator may not expect their logo to be public.
- The metadata is not writable through the settings endpoint: it describes
  bytes on disk, and hand-writing it would claim an asset that is not
  there.

`./data/branding` follows the three-step rule #303 paid for: env default +
`data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the
`mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and
closes the hole that made #303's variant invisible: the nightly archive
skips a missing directory WORDLESSLY, so the fence now demands that every
`*_DIR` the backup env declares actually travels in the archive. Verified
against the real defect — removing the line fails it by name.

Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start
so #307 is the same event with a different scope, not a second id.

Verified: api suite 103 files green (a lone `public-api` ECONNRESET under
local parallel load, green in isolation — the documented local flake);
branding suite 12 tests against a real directory; crop arithmetic unit
tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the
new section (overflow 0); and the whole flow walked in the browser: upload
→ crop 780×180 → stored as 512×118 → logo in the sidebar linking home with
the instance name as its accessible name → topbar wordmark following
`instance.name` → light logo still shown under `data-theme="dark"`.
2026-08-01 19:30:52 +02:00
942f7b13d3 #304: scope the legal spec's status locator to its own form
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m31s
CI / Build container images (pull_request) Successful in 1m15s
CI / Auth e2e pack (pull_request) Successful in 8m57s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 36s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Failing after 7m12s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
The font manager's upload live regions made `getByRole('status')` ambiguous
on /admin, and legal.spec.ts — which asserts the legal form's success message
— started failing in the e2e pack. That is the documented trap in CLAUDE.md:
a new label or region makes an existing page-wide locator ambiguous, and the
fix is to scope the SPEC, not to drop the region a screen reader needs.

The section gets a named class for exactly that purpose.

Verified locally against the running stack: legal, fonts, admin-users,
admin-quotas and the a11y pack all pass.
2026-08-01 19:05:18 +02:00
f8c241b11a #304: custom fonts in the pickers, an admin screen, and the licence page
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 6m26s
CI / Auth e2e pack (pull_request) Has been skipped
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Has been skipped
The backend from #303 could store an operator's font but nothing could
choose one: no list endpoint outside the Site-Admin routes, no @font-face
rules for a family that only exists at runtime, and no management UI.

Found while wiring it up — a real defect in #303, invisible to its tests:
`fontStack` cannot tell an uploaded family from a deleted one, so the PDF
exporter embedded the face and then never named it. Every export of a pond
using an operator font rendered in the system font while the job reported
success. Both `fontStack` call sites now take the uploaded families
(`buildPdfHtml`, `pondFontVariables`); `pdf-html.test.ts` pins the
regression from both sides. Verified against a real Gotenberg: with the
families the PDF embeds PlayfairDisplay-Bold, without them NotoSans-Bold —
that was the whole bug, in one diff of two PDFs.

- `GET /fonts/custom` is readable by any signed-in user, not Site Admins
  only: the pickers, the licence page and the injected `@font-face` rules
  all need it, and gating it would have forced a second, admin-only UI.
- Bundled and uploaded families are told apart by their `<optgroup>`, not
  by a badge — the grouping is then part of the control's semantics, so a
  screen reader announces it and the native mobile select keeps it. Within
  each source the catalog's category grouping is preserved.
- The delete confirmation names how many ponds use the family and what
  happens to them; focus moves to it and back on cancel. Deletion stays
  unblocked (the api's decision, #303) — the ponds degrade, they do not
  break.
- The licence page grew a second table. That is what makes an attribution
  obligation satisfiable: a commercial licence that requires naming the
  foundry needs a page to name it on.

Verified in the browser end to end (upload two weights → listed and
rendered in its own font → chosen in a pond → page renders in it → deleted
→ pond falls back): api suite for fonts/export 77 passed, a11y pack 11/11
locally in both schemes, lint/typecheck/i18n:check green.
2026-08-01 18:32:46 +02:00
2422f3a28f #301: sort the reflow report so the culprit cannot be buried
CI still reports 737 while the local stack is now clean, and the box list
was capped at 15 entries — all of them nav links clipped by their own
scroller. Whatever pushes the page in CI sits past that cap.

The list is now sorted by reach, marks each entry as either clipped by a
fitting scroller or actually pushing the page, and shows 40.
2026-08-01 12:31:30 +02:00
194f144797 #301: dump raw box metrics from the reflow guard
Two rounds now reported no element past the viewport edge while the
document still claimed 417px of overflow — a combination that rules out
every hypothesis I had, including my own filter.

So stop inferring. The guard now prints the html/body metrics, every
element whose own content is wider than its box (with its overflow-x, so
the intentional scrollers are distinguishable), and every box reaching
past the edge with no filtering at all. Diagnostics ride in the assertion
message, not the compared value, so they show up even when they match.
2026-08-01 12:31:30 +02:00
9fce824a8e #301: make the reflow guard report the ancestor chain
The previous run came back with an empty offender list and an unchanged
417px overflow: the filter treated everything under a scroll container as
innocent, including the container that was itself too wide. A scroller
only absolves its children when the scroller fits.

It now reports the chain from body down to the widest offender with each
box's width, so the first element wider than the viewport is visible
instead of inferred.
2026-08-01 12:31:30 +02:00
18c2ed0bfe #301: the real culprit was the jump nav, not the wide content
The first attempt fixed plausible suspects. CI measured the actual page
and named something else: six `.settings-nav__link` buttons, 417px of
page-level overflow at 320px.

`.settings-nav` already had `overflow-x: auto`, but as a flex child it
also had the default `min-width: auto` — the min-content width of the
whole jump strip. That forced the column wider than the viewport, so its
own overflow rule never had anything to scroll. `min-width: 0` is exactly
the case CLAUDE.md warns about under Reflow.

The guard now ignores elements that sit inside a scroll container. Such
content is *meant* to be wider than the viewport — reporting it buried
the one finding that mattered under twelve lines of noise, and the cap
truncated the list before it could show anything else.

The table wrapper and the wrapping settings rows from the first commit
stay. Neither was the cause here, but a table cannot shrink below its
min-content width and those rows cannot wrap on their own, so both are
hardening that holds regardless of content.
2026-08-01 12:31:30 +02:00
f938ee9880 #301: stop /settings scrolling horizontally at 320px
WCAG 2.1 SC 1.4.10 asks for no two-dimensional scrolling down to 320px,
which is also what 400% zoom on a 1280px screen produces. The layout
skeleton was already hardened for this in #165; the overflow came from
content inside the sections.

- The sessions table cannot shrink below its min-content width — four
  columns, one of them the full user-agent string. It now scrolls inside
  its own container rather than pushing the page. The container is
  focusable with a role and a name, because a scroll area that only a
  mouse can reach trades one barrier for another.
- `.settings-checkbox` rows may wrap. The accent swatches have a fixed
  size and cannot shrink, so an unwrappable row set a floor for the whole
  page width.

Adds a reflow guard to the a11y pack. axe does not cover 1.4.10 — the
criterion is not derivable from the DOM — so this is a separate check,
and it names the overflowing elements when it trips instead of only
reporting that something overflows.
2026-08-01 12:31:30 +02:00
f9149eba13 #302: the vault import test reaches its page through the sidebar
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 35s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Deploy to Test (push) Successful in 14s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m49s
CI / Import/export fidelity gate (push) Successful in 1m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
The Obsidian fixture vault contains a note called "Startseite", and the
pond now creates one too — the seeded fixtures use locale `de`. Two
consequences, and the second is the one that mattered:

- the unscoped title locator matched two sidebar entries;
- `/p/<pond>/startseite` no longer belongs to the imported note. The
  pond's own start page took that slug, so the import landed on a
  suffixed one and the test was about to assert against the wrong page.

Both are fixed by scoping to the mount page and navigating through the
sidebar instead of guessing a slug. The test stays meaningful: it then
clicks a wikilink inside the page content, which the empty auto-created
start page would not have.

CI caught this; the local run passed it. Worth remembering that a
title-based locator can go green by luck.
2026-08-01 11:29:54 +02:00
30fd1ff53b #302: the permission matrix counts the start page
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m26s
CI / Auth e2e pack (pull_request) Failing after 6m12s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Successful in 1m20s
Every pond created through the api now carries one, and the matrix pond
is created that way. The start page is an ordinary page with no grant of
its own, so it follows the pond-wide permissions: the three member
subjects each see one more, the label-restricted editor too, and the
outsider — who reaches only the explicitly public page — still sees one.

The 429 in the same run was the login rate limit, reached through the
retries of this failure rather than on its own.
2026-08-01 08:25:51 +02:00
0d95e1304e #245: mode hidden — hide profile-violating options, mark the hiding
All checks were successful
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m34s
CI / Import/export fidelity gate (push) Successful in 1m1s
CI / Build container images (pull_request) Successful in 1m13s
CI / Lint, typecheck, test (pull_request) Successful in 6m14s
CI / Auth e2e pack (pull_request) Successful in 8m31s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
In hidden (and later enforced) mode, catalog-listed controls whose only
purpose is enabling a violation are not rendered while their saved value
is compliant (the four master switches, the Nextcloud backup block);
value-listed selects keep only their compliant choices (registration
mode, new-page classification, upload policy, SVG policy). Every
affected section shows one accessible policy note (i18n de+en) so
policy is distinguishable from missing features. A value that was
already violating is surfaced exactly like in marked — never silently
hidden. The API stays unchanged; enforcement is #246. e2e: hidden half
of the marking pack (rows disappear, note visible, already-violating
row stays marked, axe WCAG A/AA clean) — verified live locally; CI runs
it against a second api (VS_NFD_MODE=hidden, same database) behind its
own static server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 19:27:17 +02:00
5fdef95f67 #244: mode marked — flag profile-violating configuration in the UI
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m34s
CI / Build container images (pull_request) Successful in 1m19s
CI / Auth e2e pack (pull_request) Successful in 8m23s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m49s
CI / Import/export fidelity gate (push) Successful in 58s
Every catalog-listed control on the admin surfaces carries an accessible
deviation marking in mode marked: text + icon under the control (never
colour alone), part of the control's accessible description
(aria-describedby), i18n de+en. The check runs against the CURRENT
control value, so a violating choice is marked before saving. Covered
controls: registration mode, new-page classification, upload policy,
SVG policy, the four master switches (api/mcp/feeds/plugins), the legal
texts (violating while empty), and the Nextcloud backup toggle on the
system panel. The profile card (#243) gains the warning summary and the
hardening-guide reference. e2e: new vs-nfd-marking pack (marked half in
CI — the e2e api now runs VS_NFD_MODE=marked, which also puts the
marked state into the a11y admin scan; off half in local default runs;
both halves verified live). hidden/enforced follow in #245/#246.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 18:49:26 +02:00
2bdb0ec2cf #224: read-trail storage — partitioning, retention, admin query path
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Auth e2e pack (pull_request) Failing after 5s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Failing after 2s
Convert read_events to monthly RANGE partitions on occurred_at, with a
DEFAULT partition as safety net: a lagging maintenance job must never
turn the trail's hard-failure semantics into an outage for classified
reads. The dedup unique pair (#223) moves to per-partition indexes
(PostgreSQL cannot carry it on the parent); a bucket spanning a month
boundary may record one duplicate — over-recording is acceptable, gaps
are not.

New daily job read-trail-maintenance (job-count fence 9 -> 10) creates
months ahead — each with its dedup index — and applies the trail's own
retention readTrail.retentionDays (default 365, deliberately independent
of audit.retentionDays): whole expired months are DROPped without
scanning, remainders deleted by range, every run audited as
read_trail.pruned (catalogue v1.2; the fence regex now admits an
underscore namespace).

Site-Admin query path GET /admin/system/read-events answers "who read
page X" and "what did user Y read" within a period — API-only by
design, documented. Growth measured and documented in data-model.md:
~1 MB per 1000 events including indexes.

Tests: retention pruning + audited deletion + admin queries on the
shared database; the partitioned shape, per-partition P2002 dedup,
months-ahead creation and DROP-based pruning against a fresh database
built by the real migration chain.

Refs #224.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 12:21:45 +02:00
868b79c8bc #213: warn on uploads to classified pages; instance policy can block
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
The attachments panel of a classified page shows a persistent notice
naming the consequence (de+en): the file inherits the page's
classification but its content carries no marking (#212). The new
instance setting classification.uploadPolicy (default warn, documented;
the VS-NfD reference configuration blocks, #227) hardens the warning
into a server-side rejection (403 classified_upload_blocked) — enforced
in the upload service, not only in the UI. Tests: warning visible in the
local attachments pack; block enforced server-side with warn/block both
ways and open pages unaffected.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:33:34 +02:00
809e071f14 #207: print stylesheet with the classification on every printed sheet
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m56s
CI / Build container images (pull_request) Successful in 1m27s
CI / Auth e2e pack (pull_request) Successful in 9m18s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m5s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
First @media print support at all: page size/margins, navigation and
interactive chrome suppressed, break behaviour for headings, tables,
code blocks, figures and plugin blocks. The VS-NfD marking runs as
header AND footer on every sheet via a real-table PrintFrame whose
thead/tfoot browsers repeat per page — @page margin boxes are
unimplemented and position:fixed places unreliably in both engines
(verified empirically); on screen the table chain renders as plain
blocks, so nothing changes visually. Verified as PDF-from-browser in
Chromium 140 and Firefox 153 (2 markings on every page of a multi-page
document); the repeatable procedure is documented in
apps/web/e2e/README.md. Unclassified pages print without a marking.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:48:44 +02:00
adceca7358 #206: show the VS-NfD marking in web view header and footer
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CI / Build container images (pull_request) Successful in 4m24s
CI / Auth e2e pack (pull_request) Successful in 8m44s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 26s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m30s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m55s
CI / Import/export fidelity gate (push) Failing after 50s
ClassificationBanner renders the fixed ADR-0022 wording above and below
the content in reading view, editor and public page view; unclassified
pages show nothing. Announced to assistive tech via a localized hidden
prefix (de+en); styled from the plain text token only, so contrast holds
in both themes and under every accent with no new color pair. Public
content endpoint now carries the classification. New seed fixture
classified-note; a11y pack asserts banner top+bottom and axe-clean in
light and dark.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:18:47 +02:00
69d9072d2c #234: retention for mail_outbox
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m51s
CI / Build container images (pull_request) Successful in 3m4s
CI / Auth e2e pack (pull_request) Successful in 8m4s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Sent mails were kept forever, and digest bodies name page titles and
actors — an unbounded copy of content-adjacent data. A new daily
mail-outbox-retention job deletes SENT rows (by sentAt) and permanently
FAILED rows (by nextAttemptAt, the last attempt's stamp) once they pass
mail.outboxRetentionDays (instance setting, default 30). PENDING rows —
including failed-but-retryable ones — stay the retry loop's alone.

Decision recorded (security.md §Privacy, residual-risk note for #231):
digest mails keep carrying page titles for now — there is no per-page
classification marking yet to key a suppression on (ADR 0022 / M32
revisits), and a VS-NfD reference configuration can leave SMTP
unconfigured entirely.

Job-count fence in system.spec: 8 -> 9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 22:17:28 +02:00
ff505bc752 #233: prune conversion job payloads for every job kind
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m12s
CI / Build container images (pull_request) Successful in 3m28s
CI / Auth e2e pack (pull_request) Successful in 8m33s
CI / Import/export fidelity gate (pull_request) Successful in 1m2s
CD / Build and push images (push) Successful in 29s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Failing after 5m9s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
The raw input/result bytes of import/export conversion jobs were kept
forever; a deleted classified page could live on inside its last export.
A new daily conversion-payload-prune job nulls both once a finished
(succeeded or failed) job passes conversion.payloadRetentionDays
(instance setting, default 30) — the row survives for status/audit.
PENDING and RUNNING rows keep their payload, so the worker's stale-lock
recovery path is untouched; a hand-requeued pruned job fails finally
via conversionInputOf instead of crashing the worker.

The input column becomes nullable; the migration backfills by clearing
payloads of jobs already finished longer ago than the default period
(recent results stay downloadable until they age out).

Job-count fence in system.spec: 7 -> 8 (new scheduler registration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 22:12:32 +02:00
ed2225bb77 #196: audit-trail retention job
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m5s
CI / Build container images (pull_request) Successful in 2m48s
CI / Auth e2e pack (pull_request) Successful in 7m50s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 15s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 5m11s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
audit.retentionDays (instance setting, default 365) bounds the audit_log:
the daily audit-retention job deletes entries past the period and records
the deletion itself (audit.pruned with count, cutoff and period) so a gap
in the trail is always explainable. Lives in its own AuditRetentionService
because the settings service audits its writes - folding retention into
AuditService would close a constructor cycle. The read-access trail
(#222-#225) is deliberately not covered; it gets its own period.

security.md gains the Logging section the schema has cited for a while;
the maintenance-job fence moves 6 -> 7 (the deliberate new row).

Refs #196

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 14:59:28 +02:00
02c1f18fe1 adjust the maintenance-job count fence: 6 jobs with the orphan sweep
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m58s
CI / Auth e2e pack (pull_request) Successful in 7m50s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CI / Build container images (pull_request) Successful in 1m12s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m21s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 5m5s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m36s
CI / Import/export fidelity gate (push) Successful in 55s
The system panel spec pins the registered-job count on purpose; the
orphan-file-sweep registration (#194) is the deliberate sixth row (CI
run 493 caught exactly this, 14x resolved to 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 13:31:03 +02:00
d32c8c3730 #189: make the CSRF origin check fail closed
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 1m51s
CI / Auth e2e pack (pull_request) Has been skipped
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Has been skipped
A cookie-carrying mutation without Origin and Referer (or with an
unparsable one) is now rejected with 403 csrf_origin_mismatch instead
of passing unchecked. The exception for non-browser clients stays
structural: PAT/bearer requests carry no session cookie and never reach
the check, and a request that does carry the cookie is always checked.

The test harness injects the matching Origin (supertest simulates a
browser page of this instance) with an explicit suppression header for
the negative cases; the Playwright fixture contexts send the header on
their manual seeding calls; release-qa.sh pins APP_BASE_URL and sends
the matching Origin. Dedicated spec covers: missing headers 403,
mismatch 403, unparsable 403, match passes, GETs untouched, PAT
mutation without headers passes, cookie+bearer still checked.

Refs #189

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 09:34:44 +02:00
b5d2a436e0 #186: pond accent theming — scoped derivation, cascade pond > user > default
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m44s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 10m50s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 4m2s
CI / Lint, typecheck, test (push) Successful in 4m47s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 14s
CI / Auth e2e pack (push) Successful in 10m7s
CI / Import/export fidelity gate (push) Successful in 56s
Release / Build release images and notes (push) Successful in 1m11s
Release / Release-candidate operations QA (push) Successful in 1m0s
Prod deploy / Deploy the released images to Prod (push) Successful in 17s
pondSettingsSchema gains theme = { accent: '#rrggbb' | null } (null =
inherit the viewer's theme), exposed as a top-level key of the flat
updatePondInputSchema and included in the PondsService settings merge
(the known silent-no-op pitfall). The server validates only the hex;
conformance arises at render time: PondThemeScope (mounted around the
page content next to PondFontScope) derives the accent pair for the
EFFECTIVE mode via useEffectiveTheme and sets it as inline custom
properties — inline beats both tokens.css and the user-theme <style>,
which IS the cascade precedence pond > user > default.

Pond settings get a PondThemeSection (inherit | presets | custom color
with per-mode preview swatches, explicit save like the font manager);
AccentSwatches extracted for reuse; i18n de+en. The no-JS public shell
stays deliberately un-themed (ADR 0018 amendment).

Tests: pond DB test (theme merge keeps fonts, invalid hex 400), e2e
pond-theme.spec (scope boundary content vs. chrome, per-mode
re-derivation, axe on the pond settings page; resets the fixture pond).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-29 09:09:36 +02:00
83a2fe470e #184: user accent theming — presets and free color as one mechanism
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m41s
CI / Build container images (pull_request) Successful in 4m4s
CI / Auth e2e pack (pull_request) Successful in 11m40s
CI / Import/export fidelity gate (pull_request) Successful in 52s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
apply-theme.ts derives BOTH modes' accent tokens from the stored choice
(ui.theme.accent: preset id or {custom:'#hex'}) and writes them as
<style id="user-theme"> with :root:root + :root:root[data-theme='dark']
blocks — the doubled :root beats tokens.css regardless of document
order, since theme-init.js injects the ui.theme.css cache during <head>
parsing, before the bundle styles. The default preset means NO override
(hand-tuned tokens.css values stay). main.tsx re-derives from the
choice at startup, healing stale caches after app updates.

Settings: accent radiogroup inside the Appearance section (visible
names, color never the only cue) with per-mode preview swatches on
each mode's canonical background, plus a custom color input; i18n
de+en. The second fieldset made bare .settings-fieldset locators
ambiguous — theme specs now scope via input[name] (fence stays).

Tests: apply-theme unit pack, BASE_PALETTE<->tokens.css drift fence in
theme-contrast.test.ts, e2e theme-accent.spec (instant apply, pre-paint
persistence, default removes override, axe smoke with garish yellow in
both modes). ADR 0018 amendment documents the stage-B details.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-29 08:59:27 +02:00
b799ad180b #182: top-bar theme toggle — cycle light/dark/system without a menu
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m40s
CI / Build container images (pull_request) Successful in 4m1s
CI / Auth e2e pack (pull_request) Successful in 8m30s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
An IconButton between the notifications bell and the user menu cycles
the theme mode in radio order (sun/moon/monitor mirror the CURRENT
choice). New useThemeMode() hook is the single write path (persist +
apply + same-document event), so the settings radios and the toggle
stay in sync; AppearanceSection now uses it too. Also rendered for
signed-out visitors — the mode is a device-local preference. i18n de+en;
unit tests for cycle/setter, theme.spec covers cycling, radio sync,
persistence, and the signed-out top bar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-29 08:43:42 +02:00
77df813f16 #180: settings jump-nav fence — nine sections since the Appearance section
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m38s
CI / Build container images (pull_request) Successful in 1m29s
CI / Auth e2e pack (pull_request) Successful in 7m44s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 2m36s
CD / Promote to Int (push) Successful in 21s
CI / Lint, typecheck, test (push) Successful in 4m46s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m18s
CI / Import/export fidelity gate (push) Successful in 56s
CI run 453 caught it: settings-nav.spec.ts pins the user-settings section
count, which #180's Appearance section raised from 8 to 9. Verified
locally against a fresh e2e environment (both pack tests green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-28 21:58:43 +02:00
4e0ad82220 #180: dark-mode test fence, both-scheme a11y pack, theme e2e, ADR 0018
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m41s
CI / Build container images (pull_request) Successful in 3m57s
CI / Auth e2e pack (pull_request) Failing after 5m26s
CI / Import/export fidelity gate (pull_request) Has been skipped
theme-contrast.test.ts parses tokens.css and asserts every real UI colour
pairing (4.5:1 text, 3:1 UI) for BOTH palettes, so palette drift fails
unit tests instead of review. theme.test.ts covers resolve/apply logic
(Node >= 22 ships a shadowing undefined localStorage global — the test
brings its own in-memory storage). The a11y pack now runs its four scans
in light AND dark via emulateMedia; the new theme pack exercises the
three-way switch end to end (instant apply, reload persistence, live OS
follow in system mode, override beats OS). ADR 0018 records the theming
model broadly: modes now, accent themes by derivation later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-28 20:46:25 +02:00
3283affa67 #175: Admin-Personenliste — Aktions-Icons statt Textlinks, Reihenfolge Admin/Deaktivieren/Löschen
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m41s
CI / Build container images (pull_request) Successful in 1m9s
CI / Auth e2e pack (pull_request) Successful in 7m32s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m41s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m46s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m18s
CI / Import/export fidelity gate (push) Successful in 55s
Die Zeilen-Aktionen der Personenverwaltung sind jetzt IconButtons
(lucide): MailCheck (Bestätigung erneut senden, nur bei Ausstehend),
ShieldPlus/ShieldMinus (Zum Admin machen / Admin entfernen),
UserX/UserCheck (Deaktivieren/Aktivieren), Trash2 (Löschen) — in dieser
Reihenfolge. Das zweistufige Löschen bleibt: die Bestätigung ist
weiterhin ein roter Text-Button und erhält beim Umschalten den Fokus
(kein Fokusverlust, ADR 0017). Lokalisierte Namen kommen unverändert
aus users.json via IconButton (aria-label+title), Icons aria-hidden.
Der Admin-Bereich ist neu im a11y-CI-Pack (axe WCAG A/AA auf /admin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:41:26 +02:00
0a26572933 #171: A11y-Tooling — axe-Smoke-Pack in CI
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m38s
CI / Build container images (pull_request) Successful in 4m0s
CI / Auth e2e pack (pull_request) Successful in 7m31s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m24s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m32s
CI / Import/export fidelity gate (push) Successful in 55s
@axe-core/playwright als devDependency (exakt +2 Lockfile-Pakete,
axe-core hat null Runtime-Dependencies; Freigabe durch Stefan im Chat).
Neuer e2e-Pack a11y.spec.ts scannt Login, Lesemodus, aktiven Editor und
Nutzer-Einstellungen gegen WCAG 2.1 A/AA — jede neue Verletzung bricht
den Build (Allowlist bewusst leer, nur mit Begründung erweiterbar);
Best-Practice-Regeln bleiben außen vor. In ci.yml als eigener Schritt
mit Rate-Limit-Reset nach dem Muster der übrigen Packs verdrahtet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 15:24:56 +02:00
31b59f0fb6 #170: Statusmeldungen, Einzeltasten-Shortcuts, Bewegung
Toast-Standzeit 2,5s auf 6s (WCAG 2.2.1 — für Screenreader-/Zoom-Nutzer
kaum erfassbar). Neue Einstellungs-Sektion Bedienung mit dem Schalter
Einzeltasten-Kürzel deaktivieren (lokale Geräte-Einstellung); die
Handler von e und / prüfen sie beim Tastendruck (WCAG 2.1.4).
prefers-reduced-motion: CSS-Transitions kollabieren auf instant, die
Graph-Simulation rechnet ihr Layout synchron zu Ende statt zu animieren
(WCAG 2.2.2). settings-nav-Spec auf 8 Sektionen nachgeführt. Bewusst
KEIN zusätzliches role=status (legal.spec-Locator-Falle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:39:03 +02:00
58c19abfdd #169: Nicht-Text-Inhalte — Task-Checkboxen, Wissensgraph
Task-Checkboxen tragen in beiden Renderpfaden einen Namen: docToHtml
setzt aria-label aus dem Aufgabentext, die Editor-NodeView ebenso. Die
NodeView rendert ihr Host-Element jetzt selbst als li (ReactNodeView-
Renderer as/attrs) — TipTaps zusätzliches div-Host-Element zwischen ul
und li brach die Listensemantik; der Wrapper flacht per display:contents
ab, die #137-Pixel-Abstimmung bleibt erhalten (Selektor auf die neue
Tiefe nachgeführt, Ausrichtung nachgemessen: 1px-Versatz unverändert).
Der Wissensgraph-SVG bekommt ein beschreibendes aria-label inklusive
Verweis auf die Backlinks als gleichwertige Listenform. Der
Bild-Alt-Editor existierte bereits (Bild-Controls bei Auswahl) — kein
Änderungsbedarf. Hinweis: gecachte Seiten übernehmen das
Checkbox-Label wie bei jeder docToHtml-Änderung erst mit dem nächsten
Persist ihrer Inhalte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:35:16 +02:00
2077d92c09 #166: Skip-Link, verstecktes Seiten-h1, Resizer in die Nav-Landmarke
Skip-Link als erster Tab-Stopp springt auf #main; die angemeldete
Seitenansicht bekommt ein visually-hidden h1 (der sichtbare Titel ist
ein Input, der jetzt auch ein aria-label trägt); der Sidebar-Resizer
wandert in die nav-Landmarke (absolut an der Kante positioniert), damit
kein Inhalt außerhalb von Landmarken liegt. Zwei e2e-Locator auf das
Sidebar-Formular gescoped — das Editor-Titelfeld matcht seit dem neuen
Label ebenfalls auf /title|titel/i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:26:54 +02:00
db0e563f95 #160: Plugin-Block — Bearbeiten-Knopf nach Moduswechsel wieder da
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m39s
CI / Build container images (pull_request) Successful in 1m29s
CI / Auth e2e pack (pull_request) Successful in 7m22s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
Release / Build release images and notes (push) Successful in 1m9s
CI / Lint, typecheck, test (push) Successful in 4m48s
CI / Build container images (push) Has been skipped
Release / Release-candidate operations QA (push) Successful in 52s
Prod deploy / Deploy the released images to Prod (push) Successful in 18s
CI / Auth e2e pack (push) Successful in 7m2s
CI / Import/export fidelity gate (push) Successful in 54s
Die NodeView las editor.isEditable nur beim Mount. Die Seite mountet
immer im Lesemodus, und der Moduswechsel läuft über setEditable() —
das emittiert in TipTap nur ein update-Event, aber keine Transaction,
weshalb React-NodeViews nie neu rendern (geprüft in @tiptap/react
3.27.1: updateProps feuert nur bei Node-Änderung und Selektions-
Wechsel). Folge: die Block-Leiste blieb ohne Bearbeiten-Knopf, für
alle Block-Plugins (ChordPro, Mermaid, Excalidraw, draw.io).

Fix: useEditorEditable abonniert das update-Event und liest
isEditable reaktiv; verliert die Seite die Editierbarkeit, während
die Editier-UI des Plugins offen ist, fällt der Block auf render
zurück (der Lesemodus blendet die Leiste aus, es gäbe sonst keinen
Weg mehr heraus). Damit stimmt auch die setData-Schreibrecht-Prüfung
(editableRef) wieder.

Regressionstest im plugin-blocks-Pack: Block existiert bereits,
Seite lädt im Lesemodus, Wechsel in den Edit-Modus zeigt den Knopf
(fiel ohne Fix reproduzierbar durch); Rückweg Lesemodus→render
mitgeprüft. Die bisherigen Tests fügten Blöcke immer erst nach dem
Moduswechsel ein und konnten den Fall nicht sehen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:54:50 +02:00
58f175af32 settings-nav robust: Sofort-Sprung statt Smooth-Scroll, Spec wartet auf networkidle
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m37s
CI / Build container images (pull_request) Successful in 1m8s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Auth e2e pack (pull_request) Successful in 7m11s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m42s
CD / Promote to Int (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 5m21s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m19s
CI / Import/export fidelity gate (push) Successful in 58s
Der animierte scrollIntoView landete auf einer veralteten Zielposition,
wenn Query-Sektionen (Sessions/Tokens) während der Animation noch
wuchsen — auf dem CI-Runner deterministisch rot. Jetzt springt die
Navigation sofort; der Spec lässt die asynchronen Inhalte vor dem Klick
settlen (networkidle) und lief lokal 10× ohne Retry grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 02:11:35 +02:00
5444c39458 e2e-Fixes nach CI: section-styles-Selektor eindeutig, settings-nav-Timing
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m37s
CI / Build container images (pull_request) Successful in 1m10s
CI / Auth e2e pack (pull_request) Failing after 5m33s
CI / Import/export fidelity gate (pull_request) Has been skipped
Das Block-Menü ist seit dem eingebauten Aufgabenübersicht-Eintrag
(#154) immer sichtbar und teilt die Styling-Klasse
editor-toolbar__section-select — der section-styles-Pack adressiert
das Abschnitts-Select jetzt per :not(.editor-toolbar__block-select).
settings-nav: toBeInViewport bekommt 10 s für Smooth-Scroll auf
langsamen Runnern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 01:54:29 +02:00
e164370691 #154: Aufgabenübersicht als Kern-Block (Seite + Unterseiten)
Neuer Block-Atom task_overview (Markdown-Fence dorfteich-tasks,
HTML-Placeholder). Shared extractTaskRows liest Task-Zeilen mit Text,
Mentions (#150) und Start-/Zieldaten (#152); TasksService sammelt zur
Lesezeit den Teilbaum (rekursiv via collectSubtreeIds, canAccessPage-
Filter je Quellseite) aus Basis-State + page_updates-Log — KEINE
abgeleitete Tabelle nötig (Teilbäume sind klein, kein Drift). Neuer
auth-Endpoint GET /read/:pond/:slug/tasks; die öffentliche Ansicht
expandiert den Placeholder serverseitig zur statischen Tabelle
(Instanz-Sprache). NodeView mit Live-Tabelle und Rückschreib-Checkboxen
(optimistisch, Override bis der debounced Collab-Persist nachzieht);
Einfügen über die Block-Auswahl (eingebauter Eintrag). Unit- + DB-Tests,
neuer CI-Pack tasks.spec (voller Loop inkl. Rückschreiben end-to-end),
User-Guide-Doku en+de.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 01:20:30 +02:00
7252bd16e0 #149: Atom-Feeds für Teiche und Seiten, privat via Feed-Token
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m53s
CI / Build container images (pull_request) Successful in 4m1s
CI / Auth e2e pack (pull_request) Successful in 7m12s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m35s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 5m14s
CI / Import/export fidelity gate (push) Has been skipped
GET /public/:pond/feed.xml (zuletzt geänderte Seiten) und
GET /public/:pond/:page/feed.xml (Versions-Historie), @Public mit
404-Semantik; öffentliche Teiche anonym, nicht-öffentliche über neues
read-only Feed-Token je Nutzer als ?token=dt_feed_… (neue Tabelle
feed_tokens + Migration, Verwaltung in den Nutzer-Einstellungen,
FeedTokensSection). Öffentliche HTML-Seiten annoncieren den Teich-Feed
per link rel=alternate. DB-Tests (anonym/privat/Token-Lifecycle) und
User-Guide-Doku en+de.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:49:53 +02:00
b4247f4832 #145: Einstellungsseiten mit Sektions-Sprungnavigation
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m27s
CI / Build container images (pull_request) Successful in 3m48s
CI / Auth e2e pack (pull_request) Successful in 6m58s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Failing after 4m47s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
Neue SettingsLayout-Komponente leitet die Navigation per
MutationObserver aus den section>h2-Blöcken ab (erfasst konditionale
und komponenten-eigene Sektionen ohne Verdrahtung), sticky Leiste
neben dem Inhalt, auf schmalen Viewports horizontale Chip-Leiste;
aktive Sektion über Scroll-Position, am Seitenende gewinnt die letzte.
Auf allen vier Einstellungsseiten verdrahtet; die Admin-Grundeinstel-
lungen bekommen dafür eine eigene Überschrift. Neuer CI-Pack
settings-nav.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:34:15 +02:00
60be198e51 search.spec: Fehl-Klick entfernt — Reload IST der Weg zurück in den Lesemodus
All checks were successful
CI / Build container images (pull_request) Successful in 1m8s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m35s
CI / Build container images (push) Has been skipped
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CI / Auth e2e pack (pull_request) Successful in 6m49s
CI / Import/export fidelity gate (pull_request) Successful in 54s
Release / Build release images and notes (push) Successful in 1m12s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
CI / Import/export fidelity gate (push) Successful in 54s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m15s
Release / Release-candidate operations QA (push) Successful in 45s
CI / Auth e2e pack (push) Successful in 6m40s
Der Spec klickte nach page.reload() den Mode-Toggle „um den Edit-Modus zu
verlassen" — nach dem Reload ist die Seite aber schon im Lesemodus (React-
State resettet), der Klick wechselte also HINEIN. Das passierte jahrelang
folgenlos, weil der Fokus auf dem Toggle-Button blieb und „/" die Suche
öffnete. Seit dem Editor-Auto-Fokus (PR #140) landet der Fokus im Editor
und „/" wird dort zu Text — je nach Ausgang des Rennens gegen den rAF-
verzögerten Fokus mal grün (Läufe 381/385), mal rot (383/387/389/390).
Fix: den Klick streichen; „/" läuft im Lesemodus als globaler Shortcut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 21:31:53 +02:00
f014a61480 #133 Kommentare fest inline im Lesemodus (Slide-in-Panel ablösen)
Kommentare erscheinen jetzt fest im Lesefluss zwischen Backlinks und
lokalem Graph statt in einem ein-/ausblendbaren Panel. Der
Kopfleisten-Toggle (Icon + Unread-Badge) entfällt.

Frontend:
- CommentsPanel → CommentsSection (Inline-Sektion, ohne Panel-Chrome/
  Close-Knopf; markiert beim Sichtbarwerden als gelesen). Neue
  Read-only-Variante PublicComments für die anonyme öffentliche Ansicht.
- Umzug auf die äußere Ebene in PageEditorPage (view-Modus, zwischen
  BacklinksPanel und LocalGraphPanel). Das Schreibrecht (collab rw) wird
  per onWriteAccess aus dem inneren PageEditor hochgereicht, damit die
  äußere Ebene den Composer bei commentPolicy=editors korrekt zeigt/
  verbirgt.
- Deep-Link ?comments=1 scrollt jetzt zur Inline-Sektion statt ein Panel
  zu öffnen. Resolve/Unresolve-Knöpfe zusätzlich an mayComment gekoppelt
  (früher nur an isRoot) — Leser sehen keine 403-Knöpfe mehr; Read-only
  blendet alle Aktions-Controls aus.
- CSS comments-panel* → comments-section*; tote Unread-Badge-Regeln raus.

Backend:
- GET /public/:pondSlug/:pageSlug/comments (@Public), read-only. Nutzt den
  vorhandenen resolve()-Pfad (erzwingt ggf. anonymen Lesezugriff → nicht
  öffentliche Seiten 404en) und CommentsService.list. PublicModule
  importiert CommentsModule.

Tests: public.e2e.db.test.ts um anonymen Kommentar-Lesezugriff + 404-Fälle
ergänzt (grün gegen frische Test-DB); comments.spec.ts auf die Inline-UI
umgestellt. typecheck/lint/i18n:check grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 01:22:22 +02:00
6c98a71d34 Favorites: personal page stars, golden icons, sidebar filter (#132)
Some checks failed
CD / Build and push images (push) Successful in 3m57s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Failing after 2m51s
CI / Import/export fidelity gate (push) Has been skipped
Semantics changed from the issue during planning (documented there,
comment 1192): favorites are PERSONAL per user, not pond-wide — the
sys-fav label approach is dropped entirely. Storage is a page_favorites
table (userId+pageId, FK cascade); PUT/DELETE /pages/:id/favorite
toggles idempotently and needs read access only (#60 404 semantics —
a star is a note-to-self, not a page modification), GET
/ponds/:id/favorites lists the account's stars sliced to still-readable
pages. Trashed pages keep their rows, so restore keeps the star; purge
cascades it away.

Web: one shared ['favorites', pondId] query feeds the TopBar star
(between labels and history, golden when set), the golden tree icons in
the sidebar, and a latching "Favorites" filter button next to the view
switch that narrows either view (combinable with the label filter).
No public-API/MCP exposure — with the label approach gone, that parity
is no longer free; favorites stay UI-only for now.

New favorites e2e pack (star toggle, golden icon, filter, per-user
isolation) wired into CI; DB suite covers the round-trip, read gating,
and the trash/restore/purge lifecycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:03:55 +02:00
48d4c60af7 Trash: checkbox multi-select with bulk restore and purge (#128)
Each trash row gets a checkbox, a toolbar above the list offers
"select all" (native indeterminate for partial selections) and the two
bulk actions; bulk purge confirms with the selection count (pluralized).
Processing is sequential on purpose — purge promotes leftover children
(#107), so concurrent tree mutations would race. Failures don't strand
the rest: the loop keeps going, failed pages stay selected for a retry,
and an alert banner reports the count. Single-row actions run through
the same path, which also fixes their previously unhandled rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:03:35 +02:00
36cdd4fbca Editor: confirm snapshots with a toast; wire ui.toast for plugins (#130)
Cmd/Ctrl+S used to snapshot silently. A new app-wide ToastProvider
(components/Toast.tsx) owns a bottom-center stack — permanent polite
live region, auto-dismiss after 2.5 s, click to dismiss early, error
variant. Both snapshot paths (the keyboard chords in PageEditorPage and
the save-version TopBar button) now confirm with the version name when
there is one, and their failure alert becomes an error toast.

The plugin host capability ui.toast (declared since #74, wired
nowhere) connects to the same stack: PluginBlockScope carries the
showToast handle, plugin-block passes it into the sandbox context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:03:17 +02:00
d23e5dc10c Pond graph: size the canvas to the viewport, not the width ratio (#131)
The SVG scaled to container width with height following the fixed
800×560 viewBox ratio — on wide windows the graph grew taller than the
viewport, the page got a scrollbar, and wheel-zoom scrolled along.
The graph page is now a flex column filling the main column; the canvas
takes the remaining height (flex: 1, min-height: 0), a ResizeObserver
feeds its measured size to ForceGraph as width/height, and the SVG
fills it exactly. LocalGraphPanel keeps its fixed defaults. Scope
deliberately layout-only (issue comment 1187): with no scrollbar there
is nothing for the wheel to scroll, so no non-passive listener needed.
The graph pack now asserts the main column does not overflow
vertically on the graph route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:02:56 +02:00
0428892ef2 Editor shortcuts: "e" edits, the platform chord+S snapshots versions
In reading mode a plain "e" (guarded against typing targets) switches
to edit mode. In edit mode the platform's native chord — Cmd on macOS,
Ctrl elsewhere — +S saves an unnamed manual snapshot in place, and
+Shift+S asks for a name and returns to reading mode; both always
swallow the browser's save dialog. The shared isTypingTarget guard
moves from TopBar into lib/keyboard.ts next to the new modifier helper.

Unnamed snapshots needed the API to accept them: the version label is
optional now (trigger stays MANUAL, label null), and the history list's
existing null-label fallback text becomes "Manueller Schnappschuss" /
"Manual snapshot" — it only ever shows for exactly those. DB test for
the label-less path, e2e coverage in the CI content pack.

Fixes #125

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 13:17:09 +02:00
1781f12f6e Knowledge graph: live Obsidian-like simulation with tunable physics
All checks were successful
CD / Build and push images (push) Successful in 4m2s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Successful in 4m28s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 6m42s
CI / Import/export fidelity gate (push) Successful in 49s
Release / Build release images and notes (push) Successful in 1m7s
Release / Release-candidate operations QA (push) Successful in 42s
Prod deploy / Deploy the released images to Prod (push) Successful in 21s
The force layout used to run once (tick(250)) and freeze; dragging
moved a single node with no reaction from its neighbors. The simulation
now stays alive: React renders the SVG structure (testids, edge/ring
classes — the e2e contract is unchanged) while each tick writes
positions imperatively into the element refs, and it settles to rest
via alpha decay, which also keeps Playwright's stability wait happy.
Dragging pins the node (fx/fy) and reheats the physics, so the
neighborhood gets pulled along; a plain click still just opens the
page. Surviving nodes keep their positions across data refreshes, e.g.
when a phantom becomes a real page.

The graph page gains four sliders — attraction, repulsion, node size,
font size — persisted per pond (ui.graph.settings.<pondId>) with a
reset; the inner view is keyed by pond id because usePersistentState
reads its key only on mount (#108 trap). The local panel adopts those
settings (no second set of sliders) and swaps the two fixed hop
buttons for a 1–5 depth slider.

Fixes #123

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 11:29:03 +02:00
b8546eb249 Vault import: always show the label section, create labels inline
The label multiselect was gated on the pond already having labels — but
before a first import that is the common case, so the section silently
vanished and no import-wide label could be chosen. Render the fieldset
unconditionally (with a hint when empty) and add an inline create
field: POST the new label directly to get its id back, refresh the
shared label query, and tick it right away. Same pond_admin permission
as the dialog itself.

The e2e pack now creates its label through the dialog instead of the
API, covering exactly the empty-pond path that slipped through.

Fixes #121

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 10:58:50 +02:00
e356d82d51 Define the missing --color-surface token; modals were transparent
.modal (and three other rules) referenced --color-surface without a
fallback, but the token was never defined — the background declaration
was silently dropped and every modal panel rendered see-through over
the page. Define the token in tokens.css (white, matching the #fff
fallbacks other rules already carried) and pin the vault-import
dialog's opaque background in the e2e pack.

Fixes #120

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 10:55:49 +02:00
2d51a55119 QA: wire the M13 packs into CI, document the vault import (#119)
All checks were successful
CD / Build and push images (push) Successful in 2m40s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m9s
CI / Lint, typecheck, test (push) Successful in 4m23s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 12s
CI / Auth e2e pack (push) Successful in 6m15s
CI / Import/export fidelity gate (push) Successful in 47s
Release / Build release images and notes (push) Successful in 1m7s
Release / Release-candidate operations QA (push) Successful in 41s
Prod deploy / Deploy the released images to Prod (push) Successful in 16s
CI runs the two new packs after the graph pack (chained, each preceded
by the login rate-limit reset): create-missing-page.spec.ts (#115) and
import-vault.spec.ts (#117/#118).

Docs: the pond-admin guide gains a full 'Import an Obsidian vault'
chapter — the three dialog choices, and what happens to folders, links
(including the duplicate-name rule: the alphabetically first vault path
wins), tags, images, and embeds, plus the limits and the all-or-nothing
semantics. The user guide explains following a link to a page that does
not exist yet. features.md gets both bullets. German mirrors updated
throughout (English stays authoritative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:28:45 +02:00
704ebe48a6 Vault import dialog in the pond settings (#118)
Some checks failed
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Deploy to Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Failing after 1m0s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Has been cancelled
An admin-only 'Import an Obsidian vault' section on the pond settings
page opens a dialog with everything the #117 endpoint expects: the ZIP,
an indented mount-parent picker over the page tree (the MovePageDialog
pattern), a multi-select over the pond's label tree, and the
frontmatter radio (strip / keep as code block). Submit uploads and
polls the job with a vault-sized budget (600 x 1 s), then invalidates
pages, graph, phantom-links, and labels so the sidebar tree, graph, and
pickers show the import without a reload — and links to the mount page.

apiUploadFile now takes extra multipart fields (the options JSON);
existing callers are unchanged.

e2e import-vault.spec.ts: an admin imports the fixture vault through
the dialog and the app shows the folder tree under the mount page, a
rewritten Obsidian link navigates to the right page, the embedded image
renders, and the nested tag labels exist next to the dialog's extra
label; a plain editor gets no section at all. 3x flake-free locally
(CI wiring lands with #119).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:26:26 +02:00