Commit Graph

179 Commits

Author SHA1 Message Date
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
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
2026-07-19 11:36:17 +02:00
1b0fd21254 Editor-Toolbar: bündig an Nav fixen + Overflow-Menü über die Toolbar heben
All checks were successful
CI / Build container images (pull_request) Successful in 1m9s
CI / Auth e2e pack (pull_request) Successful in 6m45s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 6m38s
CI / Import/export fidelity gate (push) Successful in 1m10s
Zwei im Bearbeiten-Modus gemeldete Layout-Bugs:

1. Die sticky Editor-Toolbar klebte an der Padding-Kante des Scroll-
   Containers `.main` (padding-top: --space-6), also mit sichtbarer Lücke
   unter der Navigation, durch die die scrollende Seite schien. Fix:
   `top: calc(-1 * var(--space-6))` → die Toolbar pinnt bündig an die Nav.

2. Das „…"-Overflow-Menü der Navigation lag HINTER der Toolbar: `.topbar`
   steht im DOM vor `.app-body`, hatte aber keinen Stacking-Kontext, also
   malte die z-index-20-Toolbar in `.main` darüber und verdeckte
   Menüeinträge. Fix: `.topbar { position: relative; z-index: 30 }` (> 20;
   Modals mit 1100+ gewinnen weiterhin).

Beide am echten Test-Editor mit langer, scrollbarer Seite verifiziert
(gap_px=0 nach dem Scrollen; Menü vollständig über der Toolbar).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 11:08:40 +02:00
baea736616 #137 Fix: Checkbox-Absatz per :first-of-type ausrichten (nicht :first-child)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m28s
CI / Build container images (pull_request) Successful in 1m7s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CI / Auth e2e pack (pull_request) Successful in 6m44s
Im Lesemodus-Markup ist das <input> das erste Kind des <li>, also ist das
<p> nie :first-child — die Regel `li > p:first-child { margin-top: 0 }`
griff daher NICHT, das <p> behielt seine ~1em-Obermarge und der Text saß
deutlich tiefer als die Checkbox (auf Test/Int sichtbar, kein Cache-Bug).
Fix: :first-of-type/:last-of-type treffen den ersten/letzten <p>
unabhängig vom vorangehenden <input>. Beide Renderpfade abgedeckt.

Verifiziert per Harness mit exaktem <li><input><p>-DOM (alt vs. neu).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 10:50:19 +02:00
5255cdce06 Fix latenten Flake in links.service.db.test (Phantom-Sortierung)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m24s
CI / Build container images (pull_request) Successful in 3m45s
CI / Auth e2e pack (pull_request) Successful in 6m48s
CI / Import/export fidelity gate (pull_request) Successful in 50s
CI / Lint, typecheck, test (push) Successful in 4m31s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 6m45s
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 9s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
pondGraph-Test sortierte die EMPFANGENEN Phantom-Slugs, verglich aber
gegen ein UNsortiertes Literal. Da beide Slugs (ghost-<sfx>,
ghost-secret-<sfx>) den Zufalls-Suffix teilen, kippt ihre Sortierreihen-
folge auf ~1/5 der Suffixe (wenn sfx[0] > 's') → nicht-deterministischer
Fehlschlag. In CI-Lauf 372 traf es zu (Suffix „vpxwe…"). Fix: beide
Seiten sortieren. Vorbestehender Bug, unabhängig von M17–M19.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 06:12:55 +02:00
d5b895ada2 #135 Fix: /read-Route deklariert Zugriffsregel explizit (@AuthenticatedOnly)
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 4m20s
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
route-permissions.e2e.db.test.ts (#52) verlangt, dass JEDE Route ihre
Zugriffsregel explizit deklariert (PERMISSION_KEY, @Public oder
SiteAdminGuard). Der neue GET /read/:pond/:slug hatte keinen Decorator
(verließ sich auf den Default-Guard) → Coverage-Test rot in CI.
@AuthenticatedOnly() ergänzt (Session erforderlich; per-Page-Recht prüft
weiterhin der Service via resolve→canAccessPage→404).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 06:01:45 +02:00
15376d4ac2 #135 Seiten-Einbettung ![[Seite]] (Transklusion) im Lesemodus
Obsidian-Syntax `![[slug]]` (optional `![[slug|Anzeige]]`) als Seiten-
Einbettung. Im Lese- und öffentlichen Modus wird der Inhalt der Zielseite
inline gerendert; im Editier-Modus zeigt die NodeView eine Platzhalter-
Karte (Titel + Öffnen-Link).

Shared (Vorbild plugin_block):
- Neuer Block-Atom-Node `transclusion` (targetSlug + optional displayText).
- Markdown: Block-Regel für eine reine `![[…]]`-Zeile (vor `paragraph`
  registriert; mitten im Absatz greift sie bewusst nicht), Token→Node-
  Mapping, Serializer — Round-Trip stabil.
- html.ts: Platzhalter `<div class="dt-transclusion" data-transclusion>`.
- extractWikilinkSlugs erfasst jetzt auch Transklusionen → Einbettung
  zählt als Backlink/Graph-Kante.

Backend (zentraler Render-Pfad):
- PublicService expandiert Platzhalter zur gerenderten Body-HTML der
  Zielseite: SELBER Pond, read-permission-geprüft, Tiefe ≤2 + Zyklen-
  Guard (visited); Fehlend/unlesbar/zyklisch → neutraler Wikilink. Medien
  werden EINMAL über den ganzen Baum aufgelöst (kein Doppel-Processing).
- Neuer authentifizierter Endpoint GET /read/:pondSlug/:pageSlug (nicht
  @Public) liefert dieselbe gerenderte HTML — für die NodeView im
  authentifizierten Lesemodus, auch bei nicht-öffentlichen Seiten.

Web:
- NodeView `transclusion.tsx`: Editier-Modus → Karte; Lesemodus → holt
  /read/:pond/:slug und rendert den (server-sanitisierten) Inhalt inline.
- WikilinkAutocomplete unterstützt `![[` → fügt einen Transklusions-Block
  ein (statt Wikilink).
- CSS für Karte (.dt-transclusion-card) und Embed (.dt-embed), i18n de+en.

Tests: shared Round-Trip-Unit (5), public-DB-Test um Embed-Expansion
(zyklus-sicher, Fehlend→Link) erweitert — grün. typecheck/lint/i18n grün.
Visuelle Editor-Verifikation folgt auf dem Test-Stage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 04:01:18 +02:00
a84e9880bd #133 Fix: Typfehler im public-Kommentar-Test (noUncheckedIndexedAccess)
body.threads[0] ist unter noUncheckedIndexedAccess möglicherweise
undefined; per Destrukturierung + Non-null-Assertion nach dem
toHaveLength(1)-Check geglättet. (Der Fehler rutschte durch, weil der
#133-Commit nach dem Nachtragen des Tests nicht erneut getypecheckt
wurde.)

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 01:22:22 +02:00
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
2026-07-19 01:04:11 +02:00
d1d98a9575 #137 Checkbox-Aufzählungen: Textzeile vertikal ausrichten
Die Task-List-CSS war auf `.editor-content` gescoped und griff daher in
keinem Lese-Container (public/comment/legal/home/history-preview), wo
docToHtml sein `<li><input><p>`-Markup einspeist — dort blieb der Bullet
sichtbar, die Checkbox lag inline und das Block-`<p>` mit Default-
`margin: 1em 0` versetzte den Text in die nächste Zeile.

Fix: Task-List-Regeln über das eindeutige `data-type='task_list'`-Attribut
(nur von docToHtml und der Editor-NodeView erzeugt) entscopen, sodass sie
im Editor UND in allen Lese-Containern greifen; Checkbox per kleinem
margin-top auf die erste Textzeile ausrichten und Ober-/Untermarge des
Item-Absatzes neutralisieren. Deckt beide DOM-Formen ab: Lesemodus
`li > input` + `li > p`, Editor `li > label > input` + `li > div > p`.

Visuell verifiziert (Vorher/Nachher, beide Pfade).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 00:55:18 +02:00
4f79a816e4 Toast stack: drop role=status from the global live region
All checks were successful
CD / Build and push images (push) Successful in 1m10s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 6m32s
CI / Import/export fidelity gate (push) Successful in 49s
Release / Build release images and notes (push) Successful in 1m9s
Release / Release-candidate operations QA (push) Successful in 42s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
The always-mounted toast container carried role="status", so every
page-scoped getByRole('status') locator suddenly resolved to two
elements — legal.spec failed CI with a strict-mode violation. The
region keeps aria-live="polite" (announcements work the same); the
status role stays with the per-page elements that had it before.

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:02:56 +02:00
31557f09e5 Sidebar: pin the action icon row to the visible bottom (#129)
.sidebar is itself the scroll container, so the footer's margin-top:
auto only pinned the icon row to the end of the CONTENT — behind the
fold on long page lists. The row is now position: sticky with negative
bottom/side margins undoing the sidebar padding, a background, and a
top border, so the list scrolls away beneath it and the four actions
stay visible at any list length. The import progress list keeps
floating above the row (absolute within the sticky footer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-16 12:02:40 +02:00
1b9dd7d475 Give the zip-bomb rejection test headroom against loaded runners
All checks were successful
CD / Build and push images (push) Successful in 2m55s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 6m27s
CI / Import/export fidelity gate (push) Successful in 50s
Release / Build release images and notes (push) Successful in 1m9s
Release / Release-candidate operations QA (push) Successful in 42s
Prod deploy / Deploy the released images to Prod (push) Successful in 17s
Compressing and inflating the 257-MiB zero buffer is CPU-bound and
exceeded vitest's 5 s default on a busy CI runner (5.4 s — the M15 push
ran CI and CD concurrently). Flaky tests are defects (ADR 0014): the
test gets an explicit 30 s timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 13:45:47 +02:00
78a913c47b Vault import: decode ZIP names as UTF-8 and NFC-normalize them
Some checks failed
CD / Build and push images (push) Successful in 4m59s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Failing after 5m36s
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 1m21s
CD / Promote to Int (push) Successful in 11s
Real vault ZIPs broke umlauts in page titles ("Fußball zum Götzen" →
mojibake, slug fua-ball…goi-tzen): fflate honors only the ZIP UTF-8
flag, which common archivers omit, and decodes unflagged names as
Latin-1. That decoding is byte-lossless, so parseVaultZip now re-reads
any name whose chars all fit one byte as UTF-8 (a strict decoder —
genuine Latin-1 and flag-decoded precomposed chars fall back
unchanged), then NFC-normalizes: macOS zips store umlauts decomposed,
which silently broke slugify's ä→ae digraphs, wikilink matching, and
duplicate-basename detection. slugify itself also precomposes first as
defense in depth for NFD input from other paths.

Unit tests pin both cases: a hand-patched ZIP whose UTF-8 name bytes
carry no UTF-8 flag, and an NFD-named note that must come out
precomposed with an ueber- slug.

Pages already imported with garbled titles stay as they are — delete
the imported subtree and re-import after this lands (or rename by
hand).

Fixes #127

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 13:24:58 +02:00
32d4cd0e4b Footer: show the Dorfteich version next to the connection status
The Dockerfile has passed the release tag into the web build as
VITE_APP_VERSION since the beginning, but nothing consumed it — the
footer now renders it right of the live/offline icon; dev builds show
the 0.0.0-dev placeholder.

Fixes #126

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

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

Fixes #125

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 13:17:09 +02:00
44a90a53ac Sidebar: pin the four actions as an icon row at the bottom
Graph, new page, import, and trash collapse from scattered text links
into one icon row pinned to the sidebar's bottom edge, in that order,
each with a hover hint (the trash reads "Papierkorb anzeigen"). The
new-page button now toggles the inline form, which still renders above
the footer with the same classes; the import trigger becomes an icon
whose progress list floats above the row so the icons stay put. All
e2e class hooks (.sidebar__new-page, .sidebar__graph-link,
.sidebar__import-*) are unchanged.

Fixes #124

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

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

Fixes #123

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
2026-07-15 11:29:03 +02:00
d4fb2a3c51 Sidebar tree: stack parents above children, compact rows, icons
The tree reused .sidebar__page-item's flex ROW from the flat list, so
the nested children <ul> sat BESIDE the parent row and align-items:
center made the parent float vertically centered next to its subtree.
Stack the two in tree view instead: parent first, children indented
below (about two characters per level).

Rows get compacter (smaller font, tighter padding) so a deep imported
vault fits on screen, and folder pages now read differently from leaf
pages at a glance: lucide Folder/FolderOpen vs FileText, with the caret
glyph upgraded to a ChevronRight that keeps the existing CSS rotation.
Caret button, classes, and aria-labels stay untouched for the e2e
contract (#101 convention: no page title in the caret label).

Fixes #122

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

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

Fixes #121

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

Fixes #120

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:26:26 +02:00
8ae010218e Obsidian vault import: endpoint, job orchestration, rollback (#117)
Some checks failed
CD / Build and push images (push) Successful in 4m5s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m21s
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 1m22s
CD / Promote to Int (push) Successful in 11s
POST /ponds/:pondId/import/vault (pond-admin-gated; a vault import
creates a subtree, uploads files, and creates labels — administration,
not everyday editing) takes the ZIP plus a JSON options field
{parentPageId?, labelIds?, frontmatterMode}. The archive is parsed at
enqueue for fast 400s; the job (new kind import_vault, riding the
existing isImportKind worker routing) re-parses and runs the #116
transform, then: containers top-down → notes (asset placeholders →
uploaded pond files; non-images become page attachments) → tags to
labels (nested tags build a label hierarchy via LabelsService, so
locking and cache invalidation apply) plus the dialog labels.

All-or-nothing: any failure hard-deletes the created pages (children
first) and removes the stored files (quota restored), then surfaces as
import_vault_invalid_zip / import_vault_too_large / quota_exceeded /
conversion_failed — and makes the worker's retry policy safe.

Supporting changes:
- conversion_jobs gains a nullable options jsonb column; enqueue takes
  kind-specific options and a maxInputBytes override (the 25 MiB
  default protects the pandoc sidecar, which a vault never touches —
  vaults use the 64 MiB upload limit).
- insertPage accepts a pre-reserved slug (the batch reserves all slugs
  up front against pond ∪ batch).
- NEW: pages born with content seed their outgoing page_links rows
  (deriveContent now returns wikilinkSlugs) — imported pages would
  otherwise stay invisible to backlinks and the graph until their
  first collab save. Collab still rewrites the rows on every save, and
  the existing phantom resolution heals batch creation order.

import-vault.e2e.db.test.ts (4 tests, real worker drained): gating +
input rejection, the full fixture import (tree under a mount page,
collision suffixes, link rows incl. phantom, nested tag labels, extra
label everywhere, frontmatter stripped, image embedded + PDF attached),
complete quota rollback, and a clean re-import with fresh suffixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:13:11 +02:00
269b36c761 Fix the gates the #116 commit skipped past
Some checks failed
CD / Build and push images (push) Successful in 2m53s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m11s
CI / Lint, typecheck, test (push) Failing after 4m22s
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 / Promote to Int (push) Successful in 11s
The asset type lost its name field during the split into path+extension
(tsc error), and the fixture's .obsidian/app.json needed Prettier's
formatting. Lesson from the /srv move repeated within one day: the gate
chain must gate — never '; echo' past a failing typecheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:53:11 +02:00
09abda3ada Obsidian vault transform module (#116)
Some checks failed
CI / Lint, typecheck, test (push) Failing after 52s
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) Failing after 1m36s
CD / Deploy to Test (push) Has been skipped
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
Pure functions from vault ZIP to import plan — no DB, no DI:

- parseVaultZip: fflate unzip with the plugin-package protections
  (zip-slip rejection, incremental unpacked ceiling 256 MiB,
  parameterized for tests); dot-directories like .obsidian/ skipped;
  deterministic ordering.
- extractFrontmatter: leading --- block, tags:/tag: in scalar, inline-
  array, and block-list forms; strip mode drops the block, preserve
  re-emits it as a fenced yaml code block.
- extractInlineTags: fence- and inline-code-aware #tag / #nested/tag
  extraction and removal (headings and pure numbers untouched).
- rewriteLinks: [[Name]], [[Name|Display]], [[Name#Heading]] (fragment
  stripped), [[folder/Name]] (path match beats basename) → the FINAL
  slug with the human name as display; unresolvable → slugified
  phantom; ![[img]] and relative ![](path) → vault-asset: placeholders
  the uploader resolves (#117); non-image embeds → italic filename +
  page attachment; SVG deliberately stays an attachment (never inline,
  security.md); note embeds degrade to plain wikilinks.
- planFolders: folder chains merged at the deepest levels to fit
  MAX_PAGE_DEPTH below the mount page (merged titles read c/d).
- planSlugs: -n suffixing against existing ∪ batch; duplicate
  basenames resolve to the lexicographically first vault path.
- planVaultImport ties it together into containers + notes + the
  referenced-asset set.

Fixture vault under fixtures/import/obsidian-vault/ (umlauts,
duplicate basenames, nested tags, deep folders, embeds, code traps);
13 unit tests colocated.

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