5796b7a5dd
86 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 5796b7a5dd |
#214: OIDC Authorization Code with PKCE, Keycloak as reference IdP
External authentication (ADR 0021) built on jose (#188's vetted library) plus fetch — no new dependency enters the supply chain for a security base function. Discovery-configured; ID tokens validate against the IdP's JWKS under an explicit RS256/ES256 allowlist with issuer, audience, expiry and nonce binding. State, nonce and the PKCE verifier travel in a signed HttpOnly Lax cookie keyed by a dedicated HKDF purpose (oidc-state, ADR 0020). Deploy-level configuration (OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES/ PROVIDER_LABEL): who authenticates users is a platform decision. The login page discovers the provider via GET /auth/methods and renders the SSO button (i18n de+en). Identities use the existing slot (provider oidc:<issuer>, subject from the token). First login creates the account just-in-time — ACTIVE and mail-verified only when the IdP asserts a verified address. An existing local account is NEVER adopted silently by e-mail (account-takeover path): login refuses with oidc_link_required and the owner links explicitly via GET /auth/oidc/link (audited auth.identity_linked, catalogue v1.3). Sessions come from the one existing session service. Tests run the full flow against a protocol-faithful fake IdP: PKCE verifier at the token endpoint, JIT creation incl. personal pond, invalid state/nonce/signature/issuer/audience/expiry each rejected, the linking refusal and the explicit link flow. Verified end-to-end against a real Keycloak 26.0 (repeatable procedure documented in security.md §External authentication). Refs #214. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 183faf7710 |
#204: classification as first-class page metadata (ADR 0022)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m42s
CI / Build container images (pull_request) Successful in 3m56s
CI / Auth e2e pack (pull_request) Successful in 8m17s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 24s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 6m17s
CD / Smoke tests against Test (push) Successful in 3m32s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 8m20s
CI / Import/export fidelity gate (push) Successful in 55s
Enum field on Page (UNCLASSIFIED default, VS_NFD), migration backfills existing pages. New pages take the instance-wide default from classification.newPageDefault (admin-visible, de+en). The value rides in every PageView, so no channel needs an extra request. The field is a marking, not a protection mechanism: a test pins that permission decisions are unchanged by it. The marking wording is fixed in ADR 0022 and sourced solely from classificationMarking() in @dorfteich/shared. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| c4c84b33f9 |
#200: hard instance-wide plugins.enabled kill switch
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m38s
CI / Build container images (pull_request) Successful in 4m11s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 1m9s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Failing after 51s
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 5m37s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
plugins.enabled (instance setting, default on — plugins predate the switch; the VS-NfD reference configuration turns it off) makes every plugin surface answer 404 via a shared guard: Site-Admin install/list/mode, pond activation and plugin list, the sandbox frame and asset routes. The dropzone watcher quarantines drops instead of installing. Deliberately NOT guarded: the authenticated fallback-metadata route — it serves no plugin code and existing plugin_block nodes need it to render their declared fallback (an image fallback degrades to the neutral placeholder while off, because its bytes live on the disabled asset surface). The editor offers no plugin blocks because the pond plugin list is one of the 404ing surfaces. Admin settings panel gets the toggle (i18n de+en) with the documented api-restart note (in-process settings cache). Answers "code execution inside the zone?" with one verifiable off-switch instead of per-plugin trust machinery (#232, ADR 0025). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ |
|||
| 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 |
|||
| 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 |
|||
| 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
|
|||
| 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
|
|||
| 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 |
|||
| 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 |
|||
| 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> |
|||
| 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> |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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
|
|||
| 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 |
|||
| 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 |
|||
| 4ba4ca7ba9 |
Editor: Auto-Fokus beim Wechsel in den Edit-Modus
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m50s
CI / Build container images (pull_request) Successful in 1m16s
CI / Auth e2e pack (pull_request) Successful in 6m44s
CI / Import/export fidelity gate (pull_request) Successful in 54s
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 1m15s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 5m29s
CI / Import/export fidelity gate (push) Has been skipped
Beim Umschalten in den Bearbeiten-Modus (Stift-Icon oder Shortcut „e") landet der Cursor jetzt automatisch im Editor — bisher brauchte es einen zusätzlichen Klick, der auf neuen/leeren Seiten zudem pixelgenau den schmalen Inhaltsbereich treffen musste. Effekt feuert, sobald der Editor editierbar wird (nach setEditable), und überspringt den Fokus-Klau, wenn gerade ein Textfeld (z. B. der Seitentitel) den Fokus hält — der Moduswechsel darf den Caret nicht aus dem Titel reißen. TipTaps focus() stellt die letzte Auswahl wieder her bzw. setzt den Caret an den Anfang einer leeren Seite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 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 |
|||
| c858f12592 |
#134 Statuszeile zwischen Navigation und Artikel
Neue schlanke Statuszeile (letzte Aktualisierung · Wortzahl · geschätzte Lesezeit) zwischen Seitenkopf und Artikel — im authentifizierten Lesemodus und in der öffentlichen Ansicht. - Geteilte Komponente `PageStatusBar` (Datum via Intl in der aktiven Sprache, Lesezeit = ceil(Wörter/200), Singular/Plural, Lesezeit ausgeblendet bei 0 Wörtern). - `countWords`/`htmlToText`-Helfer in lib/word-count.ts. - Authentifiziert (`PageEditorPage`, nur Lesemodus): Wortzahl aus dem vorhandenen Markdown-Export (geteilter Query-Key ['page-markdown']), `updatedAt` direkt von `page.data`. - Öffentlich (`PublicPageView`): Wortzahl aus dem server-gerenderten HTML per DOMParser — kein Editor-Bundle nötig; kein Backend-Change. - i18n common.statusbar (de+en), CSS `.page-statusbar` (middot-getrennt, gedämpft). Gates grün (typecheck/lint/i18n:check); visuell verifiziert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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 |
|||
| 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> |
|||
| 64e21e9f94 |
Offer creating the page on the not-found screen (#115)
Some checks failed
CD / Build and push images (push) Successful in 4m20s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m38s
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 / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Has been cancelled
Following a phantom wikilink now ends with a way out instead of a dead end: when the pond resolved and the page 404s as plain not_found, the error screen offers creating the page in place. Title = the URL slug (the PhantomPagesView mechanic), so every wikilink pointing at the address resolves; the invalidated page query then mounts the editor on the same URL. The affordance is deliberately ungated like the sidebar's new-page button — the client cannot tell 'never existed' from 'not readable' (#60), and a reader's POST surfaces as the regular 403 banner. The page_trashed branch (#31) is untouched. Rides along: PhantomPagesView now also invalidates ['pond-links'] — the graph views kept showing a just-created target as a phantom. e2e pack create-missing-page.spec.ts (CI wiring lands with #119): author a phantom link, follow it, create, backlink proves resolution; reader path asserts the 403 banner and no editor mount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| a72cb1b1c5 |
Local neighborhood graph on the page (#113)
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 2m26s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m21s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
A collapsible 'Local graph' panel joins the backlinks below the page content in read mode: the current page (highlight ring) with its wikilink neighbors in both directions, switchable between direct neighbors and two hops. Computed client-side by BFS over the cached pond-wide graph response — no second endpoint; the TanStack query is shared with the pond graph view. Phantom targets render dashed; a click navigates to the neighbor; pages without any links show no panel at all. Reuses the ForceGraph renderer from #112 unchanged. Verified live: hop toggle reveals the second-hop page, ring on the current page, click-through, and the panel's absence on a lonely page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 0308bc712d |
Drag-onto reparent, Move-to dialog, and the delete decision (#109)
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 3m58s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Successful in 4m23s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Has been cancelled
Sidebar folder view: a row now has three drop bands — the edges keep the within-group reorder, the middle band nests the dragged page under the row (appended to its new sibling group, with a drop-into outline cue). Cycle/depth refusals surface as a translated banner; successful moves are announced for screen readers. The overflow menu gains 'Move to…': a modal parent picker over the page tree (top level first, the page's own subtree disabled) that works in every sort mode. Delete now decides per case: childless pages keep the plain confirm; pages with subpages open a dialog offering promote (default wording: move subpages up) or subtree delete. The children lookup reads the CACHED pages list on purpose: an async fetch before window.confirm broke the click→confirm→DELETE rhythm the content pack (and users) rely on, and a stale childless read errs toward promote — never toward a silent subtree delete. Sidebar caret labels deliberately exclude the page title: accessible names are matched by substring in the specs (#101), and a title like 'Editor…' collided with the edit-mode toggle. Verified live: move dialog (subtree option disabled), promote and subtree delete flows; content/trash/export packs green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 15184876bd |
Sidebar folder view, label view, and the view toggle (#108)
Some checks failed
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
The sidebar now presents pages as a collapsible tree built from parentId (folder view) or grouped under the hierarchical label tree (label view, read-only; multi-label pages appear under each label, untagged ones in an 'unlabeled' group). The pond owner sets the default via a new sidebarView pond setting (PATCH-merged like the other keys); every user can override it locally (ui.sidebar.view.<pondId>), and the toggle sits above the page list. Collapse state persists per pond. New pages created while a page is open become its children — the inline form says so and sends parentId. Reordering (buttons and drag-between) now operates within one sibling group; the label filter stays a folder-view feature and falls back to the flat list while active, so the filtered order is never mistaken for a partial tree. SidebarContent is keyed by pond id so the per-pond localStorage hooks mount with the right key. e2e hooks (.sidebar__pages, .sidebar__page, reorder buttons) kept; reorder/labels/content packs green locally, plus a live smoke of nesting, collapse persistence, and both views. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 04abda5724 |
Keep the landing editor off the .legal-editor class
All checks were successful
CD / Deploy to Test (push) Successful in 10s
CD / Build and push images (push) Successful in 1m10s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m37s
CI / Import/export fidelity gate (push) Successful in 48s
Release / Build release images and notes (push) Successful in 1m6s
Release / Release-candidate operations QA (push) Successful in 41s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
The landing form reused LegalTextField, whose .legal-editor wrapper the legal e2e selects by index (nth(1) = privacy policy). Placed before the legal form it shifted those indices, so the test drove the imprint field and the published privacy text never appeared. Generalize the component to MarkdownTextField with a wrapperClass prop: legal keeps .legal-editor, the landing editor uses .markdown-field. Verified locally: legal, admin-users, admin-quotas packs green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| abf49c7c0e |
Give the landing-page save button its own label
Some checks failed
CD / Build and push images (push) Successful in 3m53s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m12s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Failing after 2m56s
CI / Import/export fidelity gate (push) Has been skipped
The landing form reused the legal namespace's "Save legal pages" label, so two buttons shared that text and the legal e2e's page-wide button lookup hit a strict-mode violation. Use a dedicated settings-namespace "Save landing page" label instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 9c64166b10 |
Editable landing page for the Site Admin
Some checks failed
CD / Build and push images (push) Successful in 3m50s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m13s
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 2m44s
CI / Import/export fidelity gate (push) Has been skipped
The public home page (/) now renders Markdown the Site Admin stores in the new home.content instance setting, through the same sanitizing pipeline as the legal pages; empty falls back to the built-in welcome text. New public GET /home/content, an Admin → Settings editor with live preview, and an e2e test covering default/configured/escaping/ admin-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 627f128ab8 |
Pond lifecycle in the UI: create shared ponds, delete from settings
Some checks failed
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m8s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Failing after 11s
CD / Promote to Int (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m40s
CI / Import/export fidelity gate (push) Successful in 54s
The pond switcher grows a "+ New pond" entry with an inline form (name + optional description, quota errors surfaced translated); the pond settings of shared ponds end in a danger section that moves the pond to the site-level trash after typing its name to confirm. Personal ponds keep hiding the section. .button--danger is now a solid red button (also fixes the admin restore button, which showed red text on the accent-green background). Manuals no longer call these actions API-only; covered by a members-pack e2e test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| c8ec549fa5 |
UI polish: frameless plugin blocks in read mode, sticky toolbar, pinned footer, icon uninstall
All checks were successful
CD / Build and push images (push) Successful in 1m11s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m5s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m35s
CI / Import/export fidelity gate (push) Successful in 47s
Release / Build release images and notes (push) Successful in 1m6s
Release / Release-candidate operations QA (push) Successful in 40s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
Three refinements from Stefan's review of the plugin work: - read mode integrates plugin output like normal content: no border, no name bar, no selection outline around plugin blocks — same principle as the frameless reading shell (M10) - the editor toolbar pins to the top of the scrolling content area on long articles instead of scrolling away (position: sticky within the main scroll container) - the app footer (connection status + legal links) moved out of the scroll container into a main-column wrapper — always visible at the bottom edge of the window on every view - the plugin uninstall buttons in the admin list are icon buttons now (Trash2, house pattern: aria-label keeps the accessible name) - lint hygiene: eslint/prettier ignore packages/plugins/*/vendor — the unpacked drawio webapp drove eslint out of memory Verified in the browser against a local stack (5/5 scripted checks: icon buttons, toolbar sticky at scroll bottom, footer pinned in edit and read mode, plugin block computed border/outline none in read mode) plus 8 layout-sensitive e2e packs re-run individually, all green (comments, collab, legal, content, plugin-admin, plugin-blocks, page-tools, plugins). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 52192eb05f |
Backup mirror to BASEL: rsync of the sets after every successful run (#84)
All checks were successful
CD / Build and push images (push) Successful in 3m51s
CI / Lint, typecheck, test (push) Successful in 4m5s
CD / Deploy to Test (push) Successful in 11s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 12s
CI / Auth e2e pack (push) Successful in 5m52s
CI / Import/export fidelity gate (push) Successful in 47s
The operator-level extra beside the admin-configured Nextcloud target (#103), unblocked now that the ONE→BASEL tunnel is stable again. - sidecar: optional mirror step (mirror.ts) driven purely by env — BACKUP_MIRROR_TARGET (rsync-over-ssh), BACKUP_MIRROR_SSH_KEY (private key on the secrets volume, never in image or repo), BACKUP_MIRROR_SSH_PORT. Runs after the prune of every successful run, so --delete aligns the remote retention with the local one (the newest-complete-set guarantee carries over). Only set files travel (db-*.dump, files-*.tar.gz); status files and bundles stay local. Host key pinned via accept-new into .mirror_known_hosts on the backups volume; fixed remote modes (dirs 750, files 640, symbolic --chmod — octal needs rsync ≥ 3, macOS dev machines ship 2.6.9). rsync + openssh-client added to the sidecar image. - status: additive `mirror` block in status.json (outcome, transferred count, lastSuccessAt carried across failures) — shown on the admin backup card; failures alert via a new backupMirrorFailed mail (de+en) while the local run still counts as succeeded. - deploy/backup-basel.md: complete BASEL-side walkthrough — dedicated user dorfteich-backup with a /home/ home and a bash login shell, explicitly avoiding the Debian backup-user (UID 34) pitfalls (nologin shell rejects rsync sessions, /var/backups home), key placement through the api container onto the secrets volume, .env values, on-demand verification. - tests: rsync-arg/stats-parsing units plus an integration suite against the real rsync binary (local target; skips where rsync is absent) — transfer, idempotent re-run (0 files), retention alignment, failure path carrying lastSuccessAt. Verified live against the real BASEL host from a native sidecar run: initial transfer, host-key pinning, retention alignment after a local prune, idempotency, and the failure path (surfaced in status.json while the local run stayed green). BASEL side provisioned per the doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 04e21a0aac |
Built-in MCP endpoint (Streamable HTTP) on top of the public API (#105)
All checks were successful
CD / Build and push images (push) Successful in 3m50s
CI / Lint, typecheck, test (push) Successful in 4m2s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m37s
CI / Import/export fidelity gate (push) Successful in 47s
AI clients talk to the instance directly at /api/mcp — under the /api/ path (deviation from the issue's literal /mcp) so every existing reverse proxy already routes it; no deployment changes anywhere. - Transport: official @modelcontextprotocol/sdk server, STATELESS — each POST builds a fresh server+transport pair, no session store, replicas stay trivial; GET/DELETE answer 405. Auth per PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After). - Own switches, independent of REST: instance mcp.enabled (admin settings, default off; off = 404, feature invisible) + pond setting mcpEnabled (pond-settings toggle, default off) — pinned independent in both directions by tests. - Tools (thin wrappers over the #104 services, same permission gates, audit-logged writes): list_ponds, list_pages, read_page, search, create_page, update_page (replace semantics through the collab-owned restore path — open editors converge), add_comment, list_labels, set_page_labels (exact replace), export_pond (link to the REST ZIP). Tool errors carry the api error codes; results carry stable slugs/ids. MCP resources stay the documented stage-2 stretch goal. - Deliberately on the SDK's low-level Server API with a hand-written tool table (mcp-tools.ts): the typed registerTool generics drove tsc out of memory in a program this size; manual Zod validation keeps the wire behavior explicit. - PublicApiService exposure filtering parameterized ('api' | 'mcp', shared pondFeatureEnabled helper) — one implementation, two switches. - Docs: "Connect Claude Code / MCP clients" section in public-api.md (claude mcp add one-liner + mcp-remote bridge for stdio clients). Verification: 8-test e2e pack driving the real MCP SDK client over Streamable HTTP against a listening api (initialize + tools/list, switch independence in both directions, anonymous/garbage 401, opt-in 404 semantics, page roundtrip incl. restore-NOTIFY, labels/comments, read scope blocked from writes with scope_required); live check through the web proxy against the seeded stack (tools list, create, read, update, search — LIVE CHECK PASSED); full api suite 61/61 files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 0c85293830 |
Public REST API v1: personal access tokens, instance switch + per-pond opt-in (#104)
Some checks failed
CI / Lint, typecheck, test (push) Failing after 1m39s
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) Successful in 3m51s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 11s
Token-authenticated machine access at /api/public/v1 — the foundation for the built-in MCP endpoint (#105). Personal access tokens: - api_tokens table (SHA-256 hash, scope read|write, optional pond restriction, expiry, revocation, throttled last-used) + migration; secrets are dt_pat_<random>, shown exactly once - lifecycle endpoints under /users/me/api-tokens (session-only — a leaked token can never mint more tokens) with audit entries api.token_created/api.token_revoked - settings UI section (create with scope/expiry/pond restriction, one-time reveal with copy, list with status + revoke), de+en Activation (404 semantics per #60 on both levels): - instance setting api.enabled (default off, admin settings switch) - pond setting apiEnabled (default off, pond settings toggle; the PondsService settings-merge learned the key — the #92 lesson) Surface (/api/public/v1, excluded from the SPA's global prefix): - me, ponds, pages (list/read as Markdown+HTML, create from Markdown via the shared pipeline, PATCH title/content, DELETE to trash), search (permission-filtered + narrowed to exposed ponds, highlights as **…**), markdown ZIP export, labels (tree, create/rename/recolour/move/delete, assign/unassign), comments (threads, create, resolve/reopen) - content replacement travels the collab-owned document path: the new state lands as a MANUAL version "API update", then the established restore NOTIFY applies it — open editors converge, history stays append-only, no second lineage (VersionsService.replaceContent) - hand-maintained OpenAPI 3.1 document at /openapi.json, pinned to the controller by a route-coverage test in both directions Enforcement: - PublicApiGuard: instance switch → bearer PAT auth (request.user is the token's user) → per-token rate limit (429 + Retry-After) → scope (403 scope_required) → pond opt-in + token restriction - the shared PermissionGuard then applies the unchanged permission model; PageParamSource gained pondSlugParam for the slug+slug routes - no cookies anywhere → no CSRF surface (pinned by a hostile-Origin test) - every write audit-logged as api.write with the token attributed Tests/verification: - 12-test e2e pack: lifecycle, switches, permission matrix (reader/editor/outsider × scopes), restriction, page roundtrip incl. restore-NOTIFY assertion, labels, comments incl. policy, search narrowing, ZIP export, rate limit; full api suite 60/60 green (quota fixture via per-user override — never the instance default) - new collab-pack test proves an open editor converges onto an API content replacement (green against a local seeded stack) - UI smoke against the built SPA: token create/reveal/revoke, pond opt-in persists, admin switch persists (10/10) - docs/self-hosting/public-api.md + README link Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 5cef359b8f |
Nextcloud backup target: admin-configured, manual + scheduled uploads, in-app restore (#103)
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m45s
CD / Build and push images (push) Successful in 3m49s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 5m35s
CI / Import/export fidelity gate (push) Successful in 47s
Off-host backups for every self-hoster, configured entirely in the admin UI — supersedes the host-specific mirror plan behind #84. shared: - webdav.ts (new package entry like token-crypto): minimal WebDAV client with basic auth — PROPFIND (tolerant multistatus parser), MKCOL, PUT (streamed), GET, DELETE; Nextcloud DAV path derived from the plain server URL, explicit DAV bases pass through - backup-status.ts: additive remote-upload status in status.json, the restore-status.json contract (running/succeeded/failed + staleness bound), the backup_command/backup_maintenance NOTIFY channels, and the one-bundle-per-set naming (dorfteich-backup-<id>.tar.gz) - backup-set.ts moved here from apps/backup (api lists local sets) backup sidecar: - reads the backup.* instance settings directly from the database (admin changes apply next run; local retention row overrides the env) and the app password from the secret store - after each successful set: bundle dump + files archive + manifest into ONE self-contained tar.gz, upload via WebDAV per schedule (off/daily/weekly; manual runs always upload), prune remote bundles — never the newest — and record the outcome in status.json; upload failures alert via a new backupUploadFailed mail (de+en) - command listener on backup_command (run / restore) with a serial queue against the nightly timer - restore orchestrator: restore-status.json → maintenance NOTIFY → grace → (remote: download + manifest-verify bundle) → terminate other DB connections → shared perform-restore path (same code as restore.sh) → final status + maintenance exit api: - MaintenanceGuard (global, registered before the setup gate): 503 maintenance_mode while restore-status says running; health endpoints and the new public GET /backup/restore-status stay exempt; a stale running state (crashed sidecar) unblocks after 30 min - MaintenanceStateService watches the file and restarts the api after a successful restore (fresh caches, migrate-on-start for older dumps); main.ts refuses to touch the database while a restore runs — a container restarting mid-restore must not race pg_restore with migrate deploy - worker sweeps (conversion, mail outbox, scheduler) catch transient database failures instead of dying on an unhandled rejection — the restore's connection termination crashed the api in verification - backup admin endpoints under /admin/system/backup: settings (live connection test before save, password write-only into the secret store), nextcloud/test, sets (local via the ro backups mount + remote via WebDAV), run + restore (type-to-confirm backstop, source validation) — commands travel as NOTIFY payloads; audit actions backup.settings_changed/run_triggered/restore_requested - readyz: new warning-level backup_remote check while a target is configured (26 h daily / 170 h weekly bound) collab: - maintenance listener: on enter, persist + close every live session and refuse new connections until exit (failsafe timeout 30 min) — no in-memory document may write pre-restore content back afterwards web: - Admin → System backup section: status card with remote facts and a "Back up now" button, the Nextcloud settings form with test button, and the restore picker (local + remote sets, type-to-confirm) - global maintenance screen: any 503 maintenance_mode flips the SPA to a status page polling the exempt endpoint, reloading when the instance returns Verified end-to-end against a live stack (fresh DB, native api + sidecar, fake WebDAV server): configure → test → manual backup → bundle upload → readyz/sets/status surfaces → remote restore with maintenance gate, marker rollback and api restart; suites: shared 21, backup 9, collab 11, api 58 files green, lint + i18n:check + typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 83fa23bbf9 |
Polish round 2: content footer, dismissable menus, manual versions, substring search, icon actions in settings (M10 follow-up)
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m35s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m44s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 5m36s
CI / Import/export fidelity gate (push) Successful in 46s
- content footer: the collab status is an icon (wifi/off/refresh, localized tooltip + visually-hidden text, class/data-status hooks kept for e2e) on the left, the legal links right-aligned; read mode drops the editor frame and its inner padding, edit mode keeps it - menus (page overflow, user, notifications bell, pond switcher) close on outside click and Escape via a shared useDismissable hook; the bell got its missing tooltip - side panels (labels, history) stack vertically in one column - edit mode gains a Save-version icon (prompt for the name, POST /pages/:id/versions); the history panel lists contributors by display name — more than three collapse to two plus an expandable ellipsis (PageVersionView.contributors resolved server-side, deleted users drop out) - search finds partial words via a LIKE fallback next to the tsquery (FTS matches still rank first; regression-pinned in the db pack), and the recent-searches list has a clear button - pond owners create labels directly in the label picker (plus a permanent link to the full manager); add/remove/delete buttons across the pond settings (members, access rules, labels, files) and the watch/unwatch toggles in pond/user settings are icon buttons now — class hooks and accessible names unchanged for the e2e packs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| e740ea6c01 |
Move live presence into the TopBar, signed-in only (#102)
All checks were successful
CD / Build and push images (push) Successful in 1m39s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 46s
- the TopBar registers a presence slot (only rendered for signed-in users) next to the page-actions slot; PageEditor portals the PresenceStrip into it — behavior unchanged (initials avatars, max 5 + overflow, viewer badge, hidden when empty, both view and edit mode) - pinned guarantee: public.spec asserts the anonymous read path opens no /collab websocket and renders no presence data Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 65f30a5231 |
Move page actions into the TopBar as self-hosted icon buttons (#101)
Some checks failed
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
- lucide-react (MIT, tree-shaken, compiled into the bundle — no runtime requests; fonts.spec's off-origin assertion covers the page route) - page-actions slot: TopBar registers a DOM element via context, the active page portals its actions into it, TopBar stays page-agnostic - PageActions: mode toggle, watch (WatchToggle icon variant), comments (unread badge kept), attachments, plugin page tools, labels, history as icon buttons with localized aria-label+tooltip (de+en), plus an overflow menu for markdown copy/download, docx/odt/pdf export and the destructive delete (confirm kept) - page header keeps only the title; the editor-shell tools row is gone; panel state lives in PageEditorPage now - hamburger/search/bell adopt the same icon set - e2e: content/export open the overflow menu; class hooks (editor-page__mode-toggle, editor-shell__*-toggle, editor-page__labels-toggle, editor-page__export) kept stable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 9674c0bae2 |
Batch notifications into localized e-mail digests with unsubscribe (#95)
Some checks failed
CI / Lint, typecheck, test (push) Successful in 3m41s
CD / Build and push images (push) Successful in 3m44s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Failing after 2m53s
CI / Import/export fidelity gate (push) Has been skipped
New per-user digestFrequency (hourly default | daily | off) on the profile and in the settings UI. A scheduler job (15 min cadence) mails a user once their oldest unread, unmailed notification exceeds the cadence window: one localized mail per batch, grouped per pond then per page with actor names and change/comment counts, enqueued through the mail outbox. Sending marks the batch mailed — never read — and re-checks page read permission per entry at send time; entries the user can no longer read are dropped from the mail but still marked handled, so revoked content cannot queue forever. Every mail carries a signed, single-purpose unsubscribe link: it only flips the setting to off, renders a session-free confirmation page, and sets no cookie. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 67fb01fe2b |
Notify watchers about page changes and comments, with an in-app center (#94)
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m26s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m49s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 10s
CI / Auth e2e pack (push) Successful in 5m27s
CI / Import/export fidelity gate (push) Successful in 46s
New notifications table (payload denormalized for join-free rendering; mailed_at already prepares the #95 digests). Generation fans page events out to page and pond watchers, excluding the actors, and re-checks page read permission per watcher at delivery time — a revoked watcher gets nothing. Sources: named version snapshots (api), new comments (api), and the collab server's automatic session-close snapshots — announced over a new pg NOTIFY channel (the reverse of the established api→collab bus) consumed by a dedicated LISTEN client in the api, since the collab server has no permission resolution of its own. API: paginated list (unread first via nulls-first ordering), mark read, mark all read. UI: bell with unread badge in the top bar (30 s polling, no push in v1) and a dropdown whose entries navigate and mark themselves read; comment notifications deep-link with ?comments=1, which now opens the comments panel on load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| f4f27cbe78 |
Add watches: follow pages and ponds with auto-watch preferences (#93)
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m23s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m47s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 10s
CI / Auth e2e pack (push) Successful in 5m28s
CI / Import/export fidelity gate (push) Successful in 46s
New watches table (polymorphic target, unique per user+target; page purge
removes its rows via the trash service, and the list endpoint drops
targets the user can no longer read). Endpoints: idempotent PUT/DELETE
/watches/{page|pond}/:id gated by read access (404 hides the target),
GET state for the header toggles, and GET /users/me/watches resolving
names and links. Auto-watch hooks: creating a page and commenting
subscribe the actor, each behind a new user preference
(autoWatchOwnPages / autoWatchOnComment, default on) editable via the
profile PATCH and surfaced as checkboxes in the settings. UI: watch
toggle on the page header and the pond settings header, watch list with
unwatch in the account settings; new watches i18n namespace (de+en).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
|