Commit Graph

209 Commits

Author SHA1 Message Date
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
3c62b7b773 #197: security response headers and an explicitly restrictive CORS policy
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m15s
CI / Build container images (pull_request) Successful in 1m9s
CI / Auth e2e pack (pull_request) Successful in 7m43s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m29s
CD / Promote to Int (push) Successful in 14s
CI / Lint, typecheck, test (push) Successful in 5m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m35s
CI / Import/export fidelity gate (push) Successful in 55s
Hand-rolled middleware instead of helmet: the header set is small enough
to own, every value is a deliberate decision, and the api gains no
transitive dependency. HSTS (no includeSubDomains — the api cannot speak
for sibling subdomains), nosniff, Referrer-Policy no-referrer,
X-Frame-Options SAMEORIGIN (not DENY: the plugin sandbox frame embeds
same-origin and its CSP has no frame-ancestors, so this header governs),
and a minimal deny-all Permissions-Policy.

CORS grants no foreign origin anything; only the APP_BASE_URL origin is
ever echoed (where browsers do not consult CORS anyway), with
Vary: Origin on every response. No preflight handling — same-origin
requests never preflight, and cross-origin API access is cookie-less by
design (PAT/Bearer).

Wired via the AppModule MiddlewareConsumer so createTestApp boots the
identical middleware. Fences: security-headers.e2e.test.ts (header set,
foreign origin gets no ACAO) and a frame assertion in
plugins.e2e.db.test.ts (framing stays possible). Rationale table in
security.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 17:13:24 +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
960a806ee3 #195: trashed content leaves the search index itself
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m4s
CI / Build container images (pull_request) Successful in 2m47s
CI / Auth e2e pack (pull_request) Successful in 7m44s
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 13s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 5m9s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m53s
CI / Import/export fidelity gate (push) Successful in 53s
Trashing a page (promote and subtree modes) clears the affected search
vectors, restoring rebuilds them; pond trash clears every page vector of
the pond, pond restore reindexes only the live pages (pages trashed
inside stay out); the GDPR pseudonymization's personal-pond trash does
the same. reindexAll now converges to the invariant (clears trashed,
rebuilds live), and a one-off migration backfills vectors of
already-trashed content.

The query-side deleted_at guards stay untouched as the independent
second layer - the test proves both layers separately, including writing
a vector back onto a trashed page (simulating a future path that forgot
the clear) and asserting the query still hides it. New provider methods
removePond/reindexPond behind the SearchProvider seam.

Refs #195

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 14:07:34 +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
0bc36aa58c #194: orphan-file sweep, drop the unused Attachment.deletedAt
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m1s
CI / Build container images (pull_request) Successful in 2m48s
CI / Auth e2e pack (pull_request) Failing after 3m12s
CI / Import/export fidelity gate (pull_request) Has been skipped
Nightly sweep with two directions: attachments still unclaimed (pageId
null) after a 24 h grace period - claimed by no collab persist, page
upload, or import - are reclaimed (row, file, quota released); files on
the uploads volume without a database row (drift after a crashed
upload) are removed once older than the grace period. The grace period
protects the paste-then-insert window.

Deliberate deviation from the issue's content-reference idea, documented
in schema comment and operations.md: claimed attachments whose page
content no longer embeds them are NOT auto-deleted. The page attachments
panel lists claimed files as user-managed objects (inserting into the
document is optional there), so 'not embedded' is not 'unused' - an
auto-delete would destroy panel assets. Humans clean those up in the
panel or the pond file manager, which flags orphans already.

Attachment.deletedAt is removed by migration - deletion is hard
everywhere (sweep, purge, manual), there is no soft-delete state; the
never-true deletedAt:null filters in files/export queries went with it.

Refs #194

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 13:19:58 +02:00
402b22e05f #193: pond purge — retention job and manual Site-Admin endpoint
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m58s
CI / Build container images (pull_request) Successful in 2m47s
CI / Auth e2e pack (pull_request) Successful in 7m46s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 28s
CD / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 5m4s
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
Deletion now actually deletes: a trashed pond past the trash retention
(same clock as pages, extended trash-purge job) or purged manually via
DELETE /ponds/:id/purge (Site-Admin-only, like pond restore) is removed
with everything it holds. Files go first (idempotent rm, resumable on a
crash), then one transaction ordered around the FK actions: attachments
and labels (Restrict) precede the pond; the page delete cascades
versions, comments, content cache incl. the search vector, update log,
mentions, label assignments, favorites, outgoing links and open collab
sessions; the pond delete cascades grants, usage counters (that is the
quota correction), pond-plugin opt-ins and conversion jobs; polymorphic
watches and pond quota overrides are deleted explicitly. A purge racing
a restore or another purge is a no-op; both paths record a pond.purged
audit event.

Known residues by design, documented in operations.md: target_slug in
other ponds' page links (#235) and backups within their retention.

Refs #193

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 12:44:52 +02:00
394d1c811d #192: deploy-level backup target allowlist
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m54s
CI / Auth e2e pack (pull_request) Successful in 8m4s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 5m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m41s
CI / Import/export fidelity gate (push) Successful in 56s
BACKUP_ALLOWED_TARGETS (comma-separated destination hosts) constrains
where backups may go, enforced twice: the api rejects settings writes
and connection tests towards non-allowlisted hosts with admin-visible
error codes and resolves a non-allowlisted configured target to null,
and the sidecar enforces the same policy at the point of egress for the
WebDAV upload and the rsync mirror alike (shared policy helpers in
packages/shared/src/backup-target-policy.ts).

BREAKING: the empty default disables every remote target - backups stay
local only, the VS-NfD reference configuration (ADR 0026). Existing
deployments with a remote target must list its host or uploads and
mirror stop. The admin UI distinguishes unavailable-by-policy from
unconfigured (i18n de+en) and shows the permitted hosts.

Refs #192 (ADR 0026)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 12:17:14 +02:00
afef45732a #191: feeds.enabled instance switch, feed-token log masking
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m52s
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 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m55s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m34s
CI / Import/export fidelity gate (push) Successful in 58s
Chosen path: an instance master switch following the api.enabled/
mcp.enabled pattern — while off, both feed routes AND the feed-token
management answer 404 (existence hidden). Default ON: feeds predate the
switch, existing instances and their subscribed readers keep working;
the VS-NfD reference configuration (#227) turns it off. Admin UI gets
the toggle next to the API/MCP switches (i18n de+en).

Moving the token out of the query string is documented as rejected: a
path segment lands in the same proxy and request logs, and feed readers
cannot send headers — that is why the credential is in the URL at all.
What DID leak was our own request log (pino logs req.url): the req
serializer now masks ?token= values (common/mask-token-param.ts), so no
code path logs the credential.

Refs #191

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 11:34:43 +02:00
db4c5ce9ca #190: configurable session lifetime with a server-side idle timeout
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m53s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m53s
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 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m53s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m37s
CI / Import/export fidelity gate (push) Successful in 52s
SESSION_ABSOLUTE_HOURS (default 168 h) caps a session's total lifetime
from login: expiresAt is set once at creation and never extended — the
old sliding 30-day renewal is gone. SESSION_IDLE_HOURS (default 72 h)
ends sessions unused for that long, enforced server-side against
lastSeenAt with a write throttle scaled to the idle bound so short idle
windows still renew. Expired rows are removed on validation and the
session list applies both bounds, so idle-dead sessions never show as
active. The cookie maxAge follows the configured absolute bound.

Documented in .env.example (with the VS-NfD reference values for the
upcoming hardening guide #227), compose passes the variables through,
security.md and ADR 0007 record the amendment.

Refs #190

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 11:11:15 +02:00
214e707102 fix flaky tampered-token test: flip a significant signature character
All checks were successful
CI / Build container images (pull_request) Successful in 3m27s
CI / Auth e2e pack (pull_request) Successful in 7m51s
CI / Lint, typecheck, test (pull_request) Successful in 4m49s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m54s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m51s
CI / Import/export fidelity gate (push) Successful in 54s
The tampered-token case flipped the LAST base64url character of the
signature. Its low bits are padding that decoders ignore, so whenever a
signature ends in 'A' (~1/16 of tokens) the flip to 'B' decodes to the
same bytes and the token verifies — jose compares decoded bytes, unlike
the pre-#188 homegrown code that compared encoded strings. Reproduced
deterministically (20/20 A-ending signatures accepted the flip); CI run
477 and one local full-suite failure were this, not load. Flipping the
first character makes the tamper always significant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 09:40:05 +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
3d1f4fda53 #188: purpose-bound token keys via HKDF, jose replaces the homegrown JWT
All checks were successful
CI / Build container images (pull_request) Successful in 3m51s
CI / Auth e2e pack (pull_request) Successful in 7m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CI / Lint, typecheck, test (pull_request) Successful in 4m43s
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m54s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m39s
CI / Import/export fidelity gate (push) Successful in 59s
COLLAB_TOKEN_SECRET becomes a root key: every purpose derives its own
HKDF-SHA-256 subkey (deriveTokenKey), and no code path signs with the
root key directly. Collaboration tokens are signed and verified by jose
with HS256 as an explicit allowlist; the sign/verify API turns async at
its three call sites. Unsubscribe tokens move from a purpose-prefix
string to the structural subkey, with a documented dual-verify window
(legacy derivation accepted until 2026-11-01, covering the 90-day TTL
of links in already-sent mail).

The cross-runtime property that justified the homegrown implementation
is now proven by a test: the built CJS and ESM dist artefacts round-trip
tokens in both directions in child processes (jose v6 reaches CJS via
Node's require(esm), pinned Node 22 images). Negative tests cover
cross-purpose subkeys, root-key-signed tokens, alg:none and RS256.

Refs #188 (ADR 0020)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 06:41:11 +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
2c571f9f5e #180: dark mode — Light/Dark/System setting with token-based dark palette
The dark palette lives as a single :root[data-theme='dark'] block in
tokens.css; theme.ts and the pre-paint public/theme-init.js (external file
because the prod CSP forbids inline scripts) always resolve the stored
ui.theme.mode to a concrete data-theme, so 'system' needs no @media
duplicate and follows live OS changes via matchMedia. color-scheme flips
per theme (native controls/scrollbars), paired theme-color metas track the
effective theme, and the new Appearance settings section offers the
three-way choice as native radios (device-local, like #170). Label chips
gain a chip-outline ring so arbitrary user colors stay separated on the
dark canvas; useEffectiveTheme() is exported for the later pond-scoped
theming stage (ADR 0018).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-28 20:40:47 +02:00
5034b7a80f #180: promote remaining hardcoded base.css colors to design tokens
New tokens (light values, dark arrives with the theme block): surface-muted
(was fallback-only), danger-contrast, danger-strong, badge ok/error/warn
pairs, chip-outline. Replace the phantom --color-primary with the real
--color-accent and unify the three danger reds (#a02818/#b91c1c/#ab091e)
on --color-danger. attachments-panel maps to bg-subtle, matching its old
6%-grey fallback. Light rendering is visually unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
2026-07-28 20:37:44 +02:00
bdadfce6b9 #177: Admin-Personenliste — Aktions-Zelle bleibt Table-Cell, Flex auf Innen-Wrapper
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m39s
CI / Build container images (pull_request) Successful in 1m10s
CI / Auth e2e pack (pull_request) Successful in 7m43s
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 1m14s
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 7m24s
CI / Import/export fidelity gate (push) Successful in 55s
Release / Build release images and notes (push) Successful in 1m11s
Release / Release-candidate operations QA (push) Successful in 45s
Prod deploy / Deploy the released images to Prod (push) Successful in 16s
Die Trennlinie unter der Aktionen-Spalte endete auf Höhe der Icon-Reihe
statt am Zeilenende: display:flex direkt auf dem td nahm der Zelle ihr
table-cell-Verhalten, sie wuchs nicht mehr auf Zeilenhöhe. Das Flex-
Layout liegt jetzt auf einem Innen-Wrapper (.user-row__actions-inner);
gemessen: 0 px Bottom-Delta über alle Zellen jeder Zeile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:36:27 +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
8719b0ee1e #168: Formulare — Fehler-Verdrahtung und Namenslücken
Der Field-Baustein verdrahtet Hinweis/Fehler jetzt per aria-describedby
und aria-invalid mit dem Eingabefeld (cloneElement auf das einzelne
Kind; Fragmente bleiben unangetastet) — Screenreader nennen den Fehler
damit auch beim Feld-Fokus. Quota-Typ-Select mit Namen; die leeren
Aktions-/Erledigt-Spaltenköpfe in API-Tokens, Feed-Tokens, Sitzungen
und der Aufgabenübersicht (NodeView UND Server-Renderpfad) tragen
visually-hidden-Beschriftungen.

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
4ba7b50336 #167: Farbkontraste — Dark-Shell, Wikilink-Unterstreichung, Feld-Ränder
Die öffentliche Server-Shell bekommt AA-geprüfte Dark-Mode-Farben
(color-scheme: light dark hatte den UA dunkel rendern lassen, Links
fielen durch 1.4.3; Text 14,8:1, Links 10,1:1, Muted 8,5:1). Wikilinks
tragen eine permanente Unterstreichung — Farbe allein war das einzige
Link-Merkmal bei nur 2,5:1 Abstand zum Fließtext (1.4.1). Neues Token
--color-border-input (#7d8a97, 3,5:1/3,3:1) für Eingabefeld-Ränder
(1.4.11); Wächter-Kommentar am Favoriten-Gold.

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
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
d4d4282c55 #165: Reflow bei 320 px und Tastatur-Scrollbarkeit
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m39s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 7m17s
CI / Import/export fidelity gate (pull_request) Successful in 56s
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
Vier Ursachen des seitenweiten Horizontal-Scrollens behoben: die Topbar
saß mit min-content-Breite in der Grid-Spalte (min-width: 0 nach dem
#100-Muster) und wickelt auf schmalen Viewports auf eine zweite Zeile
(Grid-Zeile minmax, Suchtext wird zum Icon); die aufgeklappte Sidebar
liegt unter 40rem als Overlay über dem Inhalt statt ihn auf einen
Streifen zu quetschen; Footer wickelt; Titel-Input und Settings-Spalte
schrumpfen (min-width bzw. align-items: stretch im Schmal-Layout).
Der Haupt-Scrollbereich ist per tabindex=0 tastatur-scrollbar — auf den
Rechtstext-Seiten gab es sonst keinen Weg, den Inhalt zu scrollen.

Gemessen: 10 Ansichten bei 320 px ohne Dokument-Überlauf (vorher 892 px
Inhaltsbreite); e2e content/settings-nav/sidebar/legal/page-tree/search
grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:16:20 +02:00
057992faaf #164: ARIA-Semantik — Editorfläche, Autocomplete-Listboxen, Sidebar, Toolbar
Die Editorfläche bekommt einen lokalisierten zugänglichen Namen und ist
im Lesemodus role=document statt eines unbenannten Textfelds (setOptions
im selben Layout-Effekt wie setEditable). Eingeklappte Sidebar zusätzlich
inert (aria-hidden allein ließ fokussierbare Kinder im Tab-Weg). Die
li-Zwischenknoten der Listboxen (Wikilink-/Mention-Autocomplete,
Suchergebnisse) sind role=presentation, damit listbox→option wieder eine
gültige Eltern-Kind-Beziehung ist. Toolbar: Pfeiltasten-Navigation über
die Controls (native Selects behalten ihre Pfeiltasten) und ein
sprechendes Toolbar-Label statt des Absatz-Buttons-Labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:04:36 +02:00
418aafd5ec #163: Dokumentsprache und Seitentitel der SPA
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m43s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m14s
CI / Import/export fidelity gate (pull_request) Successful in 56s
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
i18n spiegelt die aktive Sprache auf <html lang> (Init + languageChanged;
der User-Locale-Wechsel in auth-context läuft über dasselbe Event). Neuer
useDocumentTitle-Hook setzt je Route einen sprechenden Titel
(Seite — Teich — Dorfteich), verdrahtet in allen Routen-Komponenten;
dynamische Titel folgen den geladenen Daten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 13:52:59 +02:00
1eca7c334c #162: Fokus-Management für Dialoge und Such-Palette
Gemeinsamer useModalFocus-Hook: Initialfokus in den Dialog, Tab/Shift-Tab
zyklisch gefangen, Fokus-Rückgabe an den Auslöser (bzw. returnFocusRef,
wenn der öffnende Menüpunkt mit dem Menü unmountet). Dialoge tragen jetzt
aria-labelledby auf ihre Überschrift und tabindex=-1 als Fokus-Fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 13:52:59 +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
f9d23bf8d0 #151: Mention-Benachrichtigungen über die Glocke
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m38s
CI / Build container images (pull_request) Successful in 12s
CI / Auth e2e pack (pull_request) Failing after 6m12s
CI / Import/export fidelity gate (pull_request) Has been skipped
Neuer Notification-Typ mentioned; abgeleitete Tabelle page_mentions
(Migration), vom Collab-Persist transaktional neu geschrieben — der
Diff gegen den Vorzustand wird als pg_notify
(page_mentions_changed) emittiert, nur NEU Erwähnte lösen aus (kein
Spam bei Folge-Saves). Der api-Listener (erweitert um den zweiten
Kanal) ruft NotificationsService.fanoutMentions: Zustellung nur nach
canAccessPage-Recheck, die Autoren (pending contributors) benachrich-
tigen sich nie selbst; Payload wie gehabt mit Actor-Namen. API-seitig
erzeugte Seiten seeden page_mentions aus deriveContent. Glocken-Text
de+en; DB-Test (Leser ja / Outsider nein / Autor nein); kompletter
Loop live verifiziert (Tippen → Persist → NOTIFY → Notification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 01:25:16 +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
3f7190ebcc #153: Stabile Task-IDs + Toggle-Rückschreibpfad über den Collab-Server
task_item bekommt ein optionales id-Attr (default null — Bestandsdocs
bleiben gültig), durchgereicht in toDOM/parseDOM und dem Lese-HTML;
der Editor vergibt/entdoppelt IDs lazy per appendTransaction
(TaskItemIds-Extension, auch gegen Copy/Paste). Neuer Kanal
TASK_TOGGLE_CHANNEL; POST /pages/:id/tasks/:taskId {checked} prüft
Schreibrecht, registriert den Toggler als pending contributor und
feuert pg_notify; neuer collab task-toggle-listener (Struktur =
restore-listener) öffnet eine DirectConnection und flippt das
checked-Attribut in einer Transaktion — offene Editoren konvergieren,
unbekannte taskId = geloggter No-op. DB-Test (NOTIFY-Payload,
Attribution, 403/404/400).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 01:03:16 +02:00
92a3b2f6d5 #152: Datums-Marker >> (Zieldatum) / << (Startdatum)
Neuer Inline-Atom date_marker {kind: due|start, date: ISO}. Markdown
kanonisch ISO (>>2026-12-31), Eingabe-Kulanz dd.mm.yyyy; Block-Guard
vor blockquote hält zeilenführende >>Daten aus dem Zitat-Parser;
ungültige Kalenderdaten bleiben Text. Editor: InputRule beim Tippen
(+Leerzeichen), Anzeige per Intl.DateTimeFormat in Nutzersprache,
Überfällig-Färbung. Die User.locale-Verdrahtung existierte bereits
(auth-context, #17) — keine Änderung nötig. 6 Unit-Tests inkl.
Task-Listen-Zeile mit Marker und Mention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:59:05 +02:00
7471fc70f7 #150: @-Mentions — Inline-Node, instanzweite User-Suche, Autocomplete
Neuer Inline-Atom mention {userId, username}: Markdown-Regel @username
(E-Mail-sicher über Wortgrenzen), Serializer, HTML-Span dt-mention,
Plain-Text für die Suche, Extraktor extractMentionUserIds. Neue
Endpoints GET /users/search (auth, min. 2 Zeichen, Limit 10,
Rate-Limit) und GET /users/brief (Batch-Auflösung für live
Anzeigenamen; gelöschte Nutzer → toter Chip). Editor: MentionView mit
Live-displayName, MentionAutocomplete (Klon des Wikilink-Musters),
Chip-CSS. 5 Unit-Tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:56:07 +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
89ffbc0e4d #147: Eigene Identität in der API klar dokumentiert
GET /api/public/v1/me existiert bereits — OpenAPI-Summary nennt jetzt
ausdrücklich die User-ID, api-guide (en+de) ebenso. MCP war bereits
paritätisch (list_ponds + Token-Identität); kein neuer Endpoint nötig.

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
d73b120d06 #148: Seitenlisten filtern nach createdSince/updatedSince
Neues pageListQuerySchema (ISO 8601, Kulanz für Datum ohne Zeit),
Query-Parameter auf interner und Public-API-Seitenliste, Prisma-where
mit gte; neue Indizes (pondId, createdAt)/(pondId, updatedAt) als
Migration. OpenAPI-Parameter, MCP-Parität (list_pages
created_since/updated_since), Doku (api-guide, mcp-guide,
public-api.md), DB-Test inkl. 400 bei ungültigem Datum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-20 00:49:52 +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
9bd25f6ce3 #146: Transparente Einbettung $[[Seite]] ohne Rahmen und Titel
Neues bare-Attr am transclusion-Node; $-Präfix in Markdown-Regel,
Serializer und Autocomplete; HTML-Placeholder trägt
data-transclusion-bare, Server-Expansion und NodeView lassen bei bare
Rahmen und Titel weg. Gleiche Tiefen-/Zyklen-/Permission-Regeln,
zählt weiter als Link. Unit- und DB-Tests ergänzt.

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
4762cb73d3 Public-Ansicht: Plugin-Fallback-SVGs auf Containerbreite skalieren
Vom Fallback-Renderer inline eingesetzte SVGs (z. B. Excalidraw-
Skizzen) tragen feste Pixelmaße und liefen auf schmalen Viewports über
den Rand — die bestehende img-Regel greift für inline-<svg> nicht.
Neue Regel .dt-plugin-fallback svg { max-width:100%; height:auto }.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 22:48:50 +02:00
75ec7f6006 CSP: data:-Fonts erlauben (eingebettete Fonts in Excalidraw-SVGs)
Excalidraw bettet beim Speichern die verwendeten Schrift-Subsets als
data:-URIs ins Snapshot-SVG ein. Die Seiten-CSP (nginx) und die
Plugin-Frame-CSP erlaubten aber nur `font-src 'self'` bzw. den
Asset-Pfad — die Handschrift fiel in der öffentlichen Ansicht und im
Snapshot-Render auf Serifen zurück (auf Prod an der ersten Demo-Skizze
sichtbar). Fix: `data:` in beiden font-src-Direktiven. data:-Fonts
lösen keinerlei Netzwerk-Request aus — die Zero-Third-Party-Garantie
(security.md) bleibt unberührt; fonts.ts-Kommentar entsprechend
präzisiert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 22:27:17 +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
d07f8bb8e1 #137 Feinjustierung: Checkbox im NodeView 3px höher (Text relativ 3px tiefer)
Stefans Feedback nach dem Nachfahren-Selektor-Fix: „noch 3px weiter
runter". Die Zeilenmetrik des Editor-/Auth-NodeViews (label-Wrapper)
setzt die Checkbox ~3px tiefer als im öffentlichen docToHtml-Markup —
daher NUR für den label-Pfad `margin-top: calc(0.25em - 3px)`; die
öffentliche Ansicht (bare input, war korrekt) bleibt bei 0.25em.
Live per Injektion auf Test vermessen: Versatz −9 → −6px, Checkbox
mittig auf der Textzeile (Zoom-Screenshot).

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
3ed3cbb806 #137 Nachfix 2: Task-Item-Absätze per Nachfahren-Selektor treffen (NodeView-Tiefe)
Some checks failed
CI / Auth e2e pack (pull_request) Successful in 6m45s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Build and push images (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Deploy to Test (push) Successful in 13s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Lint, typecheck, test (pull_request) Successful in 4m39s
CI / Build container images (pull_request) Successful in 1m13s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 5m28s
CI / Import/export fidelity gate (push) Has been skipped
Stefan sah die Checkbox-Verschiebung weiterhin — in der ANGEMELDETEN
Lese-/Bearbeiten-Ansicht. Dort rendert der TipTap-ReactNodeView das <p>
ZWEI Wrapper tief (`li > div[data-node-view-content] > div > p`), die
bisherige Kind-Kette `li > div > p` griff also nur im flachen
docToHtml-Markup der öffentlichen Ansicht. Fix: Nachfahren-Selektoren
(`li p:first-of-type` / `li p:last-of-type`) — robust gegen die
Wrapper-Tiefe beider Renderpfade.

Live am echten NodeView-DOM verifiziert (Injektion auf Test:
p-marginTop 16px→0, Checkbox bündig; öffentlicher Pfad unverändert ok).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 17:54:02 +02:00