@axe-core/playwright als devDependency (exakt +2 Lockfile-Pakete,
axe-core hat null Runtime-Dependencies; Freigabe durch Stefan im Chat).
Neuer e2e-Pack a11y.spec.ts scannt Login, Lesemodus, aktiven Editor und
Nutzer-Einstellungen gegen WCAG 2.1 A/AA — jede neue Verletzung bricht
den Build (Allowlist bewusst leer, nur mit Begründung erweiterbar);
Best-Practice-Regeln bleiben außen vor. In ci.yml als eigener Schritt
mit Rate-Limit-Reset nach dem Muster der übrigen Packs verdrahtet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
/p/:pondSlug/graph (static segment ranked above :pageSlug, same
documented reserved-slug gap as trash/settings) renders the pond's
readable wikilink graph from GET /ponds/:id/links: pages as nodes
colored by their first label (legend included, DEFAULT_LABEL_COLOR for
unlabeled), resolved links as edges, phantom targets as dashed nodes —
clicking one offers to create the page, which resolves its links.
Rendering is a self-contained SVG force graph: only d3-force is
bundled (no d3 DOM/zoom modules, zero external requests); the layout
runs synchronously to rest, zoom/pan/node-drag are plain pointer math.
SVG over canvas deliberately — every node carries a data-testid the
e2e packs can click. Ponds beyond 500 pages get a capped-view notice.
Sidebar footer links every member to the graph (trash stays
owner-only). New i18n namespace graph (de+en).
Verified live: nodes/edges/legend render, node click opens the page,
phantom click creates it and the node turns solid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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
Implements the security core of the plugin system: code-plugin surfaces
run in opaque-origin iframes (sandbox="allow-scripts", never
allow-same-origin) with a capability-filtered RPC bridge.
- api: serve a per-plugin sandbox frame document at
/plugins/:id/:version/frame with a CSP that pins every load to the
plugin's own asset path (built from APP_BASE_URL, not the request Host,
so a Host-rewriting proxy cannot break it) and forbids network access
(connect-src 'none'). Plugin assets get Access-Control-Allow-Origin: *
so the null-origin frame can load its own module bundle.
- web: sandbox-host creates the frame, wires the SDK host bridge over a
source-filtered postMessage transport, drives render under a 5 s
deadline (hung/failed plugin -> placeholder, never a frozen page), and
tears down on unmount. PluginFrame/PluginPreviewPage surface it; the
built-in ui.resize handler clamps plugin-requested heights.
- plugin-sdk: host bridge reports gate violations via onViolation and
registers a gated handler for every v1 method, so an undeclared
capability is rejected with capability_not_permitted (not
unknown_method).
- tests: SDK gate unit test; web sandbox unit tests (opaque origin,
source filtering, timeout); and the e2e security pack with a permanent
malicious fixture plugin proving no escape (DOM/cookies/storage/fetch/
undeclared capability all blocked) plus well-behaved and hung cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
Editing continues without a connection and merges conflict-free on
reconnect (ADR 0003, realtime-collaboration.md §Offline).
- y-indexeddb mirrors every opened page's Y.Doc to IndexedDB, sharing the
document with the collab provider. The local copy is discarded when the
page is left after a successful server sync (bounding IndexedDB growth)
and kept otherwise so offline edits survive to the next visit.
- vite-plugin-pwa service worker precaches the app shell (build assets only)
with a navigation fallback; `/api` and `/collab` are denylisted and there
is no runtime caching, so API responses are never cached or poisoned.
- Offline page resolution WITHOUT caching API responses: the app itself
persists the small metadata it needs to reopen a visited page (page/pond
ids + slugs, bounded LRU in localStorage) and the last signed-in user, so
after an offline tab reload the app stays signed in, resolves the page, and
restores its content from IndexedDB. Both are revalidated when the network
returns (a 401 clears the cached user).
- Local-only UI: a banner when there are edits held only on this device
(provider `onUnsyncedChanges`), de + en.
Tests: `page-cache` unit test (remember/recall + bounded eviction); a new
`offline` e2e pack (validated locally against the full stack and wired into
CI): edit, reload while offline (shell from the SW, content from IndexedDB),
assert an API call fails offline (no SW API caching), then reconnect and a
second client converges. The e2e static server serves `.webmanifest`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
Seeing other participants live (ADR 0003/0004, realtime-collaboration.md
§Awareness):
- The collaboration-caret extension renders remote carets and selections
with a name flag and a per-user colour. Colours come from a small,
hand-picked palette hashed by user id (FNV-1a), so they are stable across
sessions; a unit test asserts each palette colour clears WCAG AA contrast
(4.5:1) against the white label text.
- A presence strip at the top of the page shows an avatar (initials) per
connected participant, deduplicated by user id, with an overflow count.
Read-only participants appear in the strip (with a marker) but broadcast
no caret — the caret render suppresses read-only users — so the same
awareness feed drives both cursors and presence. Own identity (id +
display name) comes from the auth context into the awareness `user` field.
- Presence updates on every awareness change, so a disconnect drops the
participant within seconds.
The collab e2e pack gains a test: two browsers see each other in the
presence strip, one participant's named caret appears in the other's editor,
and disconnecting removes them. Validated locally against the full stack.
de + en strings and cursor/presence styles added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
The editor now edits over the collaboration server instead of REST — the
moment Dorfteich becomes collaborative (ADR 0003, realtime-collaboration.md).
Web:
- New `useCollabProvider` hook binds a page's Y.Doc to a HocuspocusProvider.
The document loads and persists through the collab server (#35); there is
no REST autosave and no REST seed (a REST seed would fork the doc lineage
and duplicate content). The collab token is fetched lazily on every
(re)connect via an async token function, so an expired token is replaced
transparently and a permission change takes effect on the next reconnect.
- Connection-state UI replaces the save indicator: connecting / connected
("Live") / reconnecting / offline, driven by provider status + navigator
online state. Read-only (`ro`) tokens make the editor non-editable with a
reason; an oversize-document stateless error (#35) surfaces a banner.
- Removed `use-page-autosave.ts` and `yjs-base64.ts` (no longer used).
API:
- `PUT /pages/:id/state` is retired and returns 410 `rest_state_write_retired`
(the criterion deferred here from #35). Collab is the sole writer of page
state; the read paths remain. Removed the now-dead `saveState` service.
e2e / CI:
- The e2e static server proxies the `/collab` WebSocket upgrade (mirrors
Caddy); vite dev gains a `/collab` ws proxy. The auth-e2e CI job starts the
collab server and runs a new collab pack.
- New `collab.spec.ts`: two browsers converge on one page (the milestone
headline), and offline edits continue locally and sync on reconnect. The
read-only live assertion is a `test.fixme` until real read-only grants
exist — under interim access seeing and modifying coincide, so no `ro`
token is issued yet (that arrives with #53). Reworked the api/trash tests
and the content editor-basics test off the retired REST write path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
A bubble menu on link selection offers "edit URL", "open in new tab",
and "remove link"; Mod-k opens the same editor for the current
selection (creating a link if there isn't one yet), and the toolbar
button does the same. Invalid protocols (e.g. javascript:) show a
localized inline error instead of silently no-oping. Pasting a URL
over selected text links it instead of replacing the text.
Links always render with target="_blank" so read mode opens them in a
new tab by default; edit mode suppresses the resulting navigate-on-
click (Mod-click still follows it), since a plain click there should
place the cursor instead.
Closes#29
TipTap is bound to the canonical ProseMirror schema (packages/shared,
#24) via a generic bridge (spec-utils.ts) that re-derives every
node/mark's attrs/parseDOM/toDOM from editorSchema instead of
duplicating them, so the editor's schema stays byte-for-byte identical
to what the api decodes Yjs states against — guarded by a schema-
fidelity + real Yjs round-trip test (@tiptap/y-tiptap client encoding
against y-prosemirror server decoding).
Route /p/:pondSlug/:pageSlug (RequireAuth) resolves the page via a new
GET /ponds/:pondId/pages/:slug endpoint, binds a local Y.Doc via
@tiptap/extension-collaboration (fragment "default"), and offers a
view/edit mode toggle (sidebar auto-hides in edit mode via a small
AppLayout context). Page state saves debounced to PUT /pages/:id/state
with a truthful saving/saved/error(retrying) indicator; title saves
separately via PATCH /pages/:id.
Toolbar covers headings, marks, lists, blockquote, code block, hr,
table (insert/row/column/header ops via prosemirror-tables), a minimal
link mark, and an image placeholder (real upload is #27/#28).
Closes#25
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The web app grows its account surface: login (with next-redirect,
unverified-hint + resend), signup (react-hook-form + shared Zod
schemas, field-level api errors, closed-registration state fed by the
new public GET /auth/registration), e-mail verification, forgot/reset
password; a settings page with profile (locale applies immediately),
password change, and active-session management; a Site-Admin page for
instance name, default locale, and registration mode. AuthProvider
holds /auth/me, applies the profile locale, and backs route guards
(RequireAuth/RequireAnonymous/RequireSiteAdmin); the top bar gains a
user menu. All strings ship in the new auth/settings namespaces (de+
en); the exception filter now preserves handler-specific error codes.
Verified live: signup → Mailpit → verify → login → profile through
the Vite proxy.
Closes#16Closes#17Closes#18Closes#19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On every push to main: build both images once (SHA + moving `test`
tag), push to the Gitea registry, SSH-deploy the Test stage, wait for
readiness, run the new Playwright smoke suite (SPA shell, web
liveness, api healthz/readyz) against https://test.dorfteich.cloud,
and on green retag the identical SHA images as `int` and deploy Int.
The CI image-build job becomes PR-only to avoid double builds on main.
Part of #8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Translation resources live in packages/shared/i18n/<lang>/<ns>.json
(common, errors) and ship with de and en. The web app initializes
react-i18next with bundled resources (?lng= wins, then the browser
language); all shell components use useTranslation and the temporary
t() stub is gone. The api localizes its uniform error bodies via a
minimal i18next instance negotiated from Accept-Language. `pnpm
i18n:check` fails CI when any key is missing in any language, backed
by tested helpers in @dorfteich/shared.
Closes#5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/web becomes a Vite + React application: React Router with home
and 404 routes, base layout (top bar, collapsible sidebar remembered
per user via localStorage, main area), CSS design tokens including the
three font slots from ADR 0016, TanStack Query, and a typed fetch
helper showing live API health on the home page. All UI strings go
through a t() stub that issue #5 replaces with i18next. The Vite dev
server proxies /api to the api dev port (3001).
Closes#4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm workspace with apps/web, apps/api, apps/collab, and
packages/shared; strict TypeScript base config, repo-wide ESLint (flat)
+ Prettier, Vitest per package, and root scripts lint/typecheck/test/
build. @dorfteich/shared ships a first health-response helper consumed
by apps/api to prove workspace linking. Existing markdown docs are
reformatted once by the new Prettier setup.
Closes#1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>