d73b120d06
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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 |
|||
| 12ff3c099f |
Delete modes and subtree trash semantics for the page tree (#107)
All checks were successful
CD / Build and push images (push) Successful in 3m59s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m20s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m15s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m42s
CI / Import/export fidelity gate (push) Successful in 47s
DELETE /pages/:id?mode=promote|subtree — promote (the default) moves the page's live children up to its parent; subtree trashes every live descendant with one timestamp and requires write permission on all of them (no partial deletes; trash access is write capability, ADR 0013). Trashed pages keep their parentId. Restore re-attaches to the nearest live ancestor (else root), which makes restore order-independent: restoring a parent afterwards never re-claims an already-restored child. Purge promotes any remaining children to the purged page's parent; the FK's SetNull stays as backstop only. tree-trash.e2e.db.test.ts covers promote, subtree + one-timestamp, the 403 descendant gate (label-DENY editor), order-independent restore, and child promotion on purge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 9d288b2ad0 |
Wire real permissions into collab tokens and revocation (#53)
All checks were successful
CD / Build and push images (push) Successful in 3m4s
CI / Lint, typecheck, test (push) Successful in 2m27s
CI / Auth e2e pack (push) Successful in 3m6s
CI / Build container images (push) Has been skipped
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
Live editing now obeys the same rules as REST: the collab-token mode comes from the shared grant resolution, anonymous visitors can join public pages, and revoking write access flips a running session to read-only within seconds. - Anonymous public tokens: `GET /pages/:id/collab-token` is `@Public()` but still permission-guarded, so a logged-out visitor gets an `ro` token where a `public` grant makes the page readable (404 otherwise). The token's `userId` is nullable (shared schema + collab context) for anonymous subjects. - Prompt revocation: the pond-level NOTIFY (#39) now also fires on label tree/assignment changes (LabelsService move/remove/assign/unassign), and the collab server closes the *actual* WebSocket instead of only sending an application-level close message. Hocuspocus' `closeConnections` leaves the socket open so the client only re-checks on its ~30s message timeout; `closeDocumentConnections` drops the socket so the client reconnects and re-authenticates with a freshly-resolved token at once — the "within seconds" downgrade the milestone promises. - Tests: the #52 fixture matrix gains anonymous cases (public grant → `ro`, none → 404); a collab db test proves an editor downgraded to reader goes read-only on reconnect (its post-downgrade edits no longer reach a peer); a new browser `collab-permissions` pack covers the read-only participant and the live downgrade end to end (new plain `fixture-editor` account). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 0c6494f209 |
Enforce permissions in API guards and retire interim access (#52)
All checks were successful
CD / Build and push images (push) Successful in 2m54s
CI / Lint, typecheck, test (push) Successful in 2m25s
CI / Auth e2e pack (push) Successful in 2m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 12s
Every route now declares its access rule explicitly and is enforced through the shared resolution algorithm (permissions.md): - PermissionGuard + decorators (@RequiresPondRole, @RequiresPagePermission, @RequiresAttachmentPermission, @AuthenticatedOnly) applied to every route; a route-enumeration test proves full coverage alongside @Public()/Site-Admin-guarded routes. - 404/403 policy (documented in README conventions): denied reads answer 404 (existence hiding), denied writes on readable things answer 403; trash views need write capability (ADR 0013). - PermissionService resolves page/pond questions via the shared resolver, with an in-process pond-context cache (grants + label parents) that is invalidated on every grant/label-tree change and TTL-bounded as a multi-process safety net. Grant changes also fire pond_access_changed for collab revalidation (#39/#53). - shared: pond-scope resolution (hasPondRole, canSeePond) next to the page resolver; grant wire schemas + GrantView. - Owner Pond-Admin grants: migration backfill for all existing ponds, created transactionally with every new pond (shared + personal + seed). - Grant CRUD under /ponds/:id/grants (pond_admin-gated) with structural and referential validation, last-admin protection, audit logs. - InterimAccessService deleted; page lists, search, backlinks, phantom links, and trash listings are filtered per page through the resolver; collab tokens are now truly ro for readers. - Fixture-matrix e2e (reader/editor/pond admin/foreign, label-deny, authenticated-subject, revoke-then-immediate-deny cache test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY |
|||
| 69b00fcf2f |
Add manual page ordering with drag-and-drop (#45)
All checks were successful
CD / Build and push images (push) Successful in 3m1s
CI / Lint, typecheck, test (push) Successful in 2m13s
CI / Auth e2e pack (push) Successful in 2m36s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Enable the third sidebar sort mode — a freely defined order. - api: `PATCH /pages/:id/position` (before/after neighbour) recomputes only the moved page's fractional `sort_key`. Pure `sort-key.ts` helpers (`nextKeyOrRebalance`, `evenlySpacedKeys`) decide between the cheap single-key path and a full pond rebalance to evenly-spaced keys when a key would exceed MAX_SORT_KEY_LENGTH or the client's neighbours are stale; rebalance runs in one transaction. Order is server-authoritative. - web: enable 'manual' in the sort-mode switch; in manual mode the owner can reorder via native drag-and-drop (drop above/below by pointer half) or the keyboard (per-row up/down buttons), each announced through an aria-live region. Reordering is hidden while a label filter narrows the list. New pages already append at the end (create uses generateKeyBetween(last, null)). Pure `reorder.ts` neighbour helpers, unit-tested. - i18n: manual sort mode + reorder strings (de + en). - tests: sort-key property test (10.000 adversarial reorders never collide or overflow — rebalance verified); reposition db test (persist, server-order, sort-mode switch keeps manual order); reorder e2e pack (keyboard reorder persists across reload + identical on a fresh read; aria-live announced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY |
|||
| 03e72242d3 |
Add label UI: tree management, page assignment, and sidebar filter (#44)
All checks were successful
CD / Build and push images (push) Successful in 2m53s
CI / Lint, typecheck, test (push) Successful in 2m8s
CI / Auth e2e pack (push) Successful in 2m31s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Build the M4 label experience on top of the #43 label API. - shared: `flattenLabelTree` (tree → depth-first list) for chip lookup, filtering, and the picker; `PageListItemView` adds each page's `labelIds` to the sidebar list response. - api: `GET /ponds/:id/pages` now includes `labelIds` per page (one grouped query), so the sidebar can render chips and filter without extra calls. - web: - Pond settings page (`/p/:pondSlug/settings`) with a `LabelManager` tree: inline create, rename, recolour (`<input type=color>`), move via a parent picker that excludes the label's own subtree, and delete that confirms then force-detaches assigned pages. Every control is a native button/input/select — the tree is fully keyboard-operable. - `LabelPicker` panel on the page editor: searchable, hierarchy-indented multi-select that assigns/unassigns immediately and refreshes the page's labels and the sidebar. - Sidebar: colored label chips on page entries (readable text via a luminance-based contrast helper) and a descendant-inclusive label filter (selecting a parent matches pages tagged with its children, via the shared `collectSubtreeIds`). Owner link to pond settings. - i18n `labels` namespace (de + en). - e2e `labels.spec.ts` (new CI pack): full lifecycle from the settings UI and picker-assign + parent-filter-includes-child. Selectors are language-independent because the UI language follows the user's locale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY |
|||
| 7d04c0b594 |
Switch the editor to live collaboration (#36)
All checks were successful
CD / Build and push images (push) Successful in 2m59s
CI / Lint, typecheck, test (push) Successful in 2m0s
CI / Auth e2e pack (push) Successful in 2m10s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
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 |
|||
| d4ebcfcfbe |
Add collaboration token issuance and connection authentication (#34)
All checks were successful
CD / Build and push images (push) Successful in 2m45s
CI / Lint, typecheck, test (push) Successful in 1m56s
CI / Auth e2e pack (push) Successful in 2m1s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 12s
The api mints a short-lived (60 s) HS256 JWT per page open after an interim
permission check; the collab server authenticates every connection with it
(ADR 0003/0007 — the only JWTs in the system).
- packages/shared: browser-safe token schema/types in `collab-token`, and the
Node `crypto` sign/verify in `token-crypto` behind its own subpath export
(`@dorfteich/shared/token-crypto`) so the web bundle never pulls in
`node:crypto`. Only HS256 is produced/accepted; the signature is checked in
constant time before any untrusted field is read.
- api: `GET /pages/:id/collab-token` (auth-required) returns
{token, mode, expiresInSeconds}; `mode` is rw/ro via the interim access
service; issuance is logged at debug level without the token value.
- collab: `onAuthenticate` verifies the token, checks the pageId matches the
document name, stores {userId, mode} context, and enforces `ro` via
Hocuspocus' read-only connection flag. Hocuspocus' own signal handling is
disabled so index.ts remains the single shutdown owner.
- Shared COLLAB_TOKEN_SECRET env for api + collab (compose, dev overlay,
.env.example, stage docs); a dev default keeps native dev/test/CI running.
Tests: shared token round-trip/rejection; api endpoint e2e (auth required,
claims, 404 for non-members/unknown ids); collab integration via
HocuspocusProvider (valid token connects; expired/tampered/mismatched-page/
wrong-secret rejected; read-only writes dropped, verified with two clients).
Closes #34
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| c9011cb44f |
Add Markdown copy, paste, and per-page export endpoint (#30)
All checks were successful
CD / Build and push images (push) Successful in 2m3s
CI / Lint, typecheck, test (push) Successful in 1m41s
CI / Auth e2e pack (push) Successful in 1m46s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
Wires docToMarkdown/markdownToDoc into the editor clipboard: copying selected content puts Markdown on text/plain alongside the browser's own HTML (so pasting into a plain-text destination yields Markdown), and pasting plain text that looks like a Markdown document converts it to rich nodes; content with real HTML on the clipboard is left to ProseMirror's normal HTML-based paste, and the heuristic requires two or more distinct Markdown-shaped lines (or a fenced code block) so ordinary prose is never mangled. Both directions need the parsed/selected doc re-hydrated against whichever schema instance is on the other side of the boundary: the canonical editorSchema (packages/shared) for markdownToDoc's output before inserting it into the live view, and the live view's schema wrapped back into editorSchema before handing a slice to docToMarkdown — they're structurally identical but not the same object, and ProseMirror's content checks are identity-based. Adds GET /pages/:id/export/markdown (downloads <slug>.md), serving the already-derived page_content_cache.markdown (#23) rather than re-decoding the Yjs state. "Copy as Markdown" and "Download as Markdown" actions in the page header both read from that same endpoint, so they always agree with each other and with the last saved state. Closes #30 |
|||
| 49beb45b3e |
Add pond sidebar with page list, sort modes, and pond switcher (#26)
All checks were successful
CD / Build and push images (push) Successful in 1m59s
CI / Lint, typecheck, test (push) Successful in 1m38s
CI / Auth e2e pack (push) Successful in 1m48s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 9s
GET /ponds/:id/pages lists a pond's pages ordered by the pond's persisted sidebarSort setting (alpha/created; manual arrives with #45). The sidebar consumes it to show the page list with an active-page highlight, an owner-only sort switch (persists via the existing PATCH /ponds/:id), and an inline "new page" flow. The top bar gains a pond switcher; a new /p/:pondSlug route gives it somewhere to land, redirecting to the pond's first page once loaded. Sidebar collapse gains a Ctrl/Cmd+\ shortcut and a slightly refined transition. Closes #26 |
|||
| 076883a9a6 |
Add TipTap page editor with REST persistence (#25)
All checks were successful
CD / Build and push images (push) Successful in 2m0s
CI / Lint, typecheck, test (push) Successful in 1m42s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
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> |
|||
| 98e159ab50 |
Add page CRUD and Yjs state persistence (#23)
All checks were successful
CD / Build and push images (push) Successful in 1m52s
CI / Lint, typecheck, test (push) Successful in 1m34s
CI / Auth e2e pack (push) Successful in 1m44s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m7s
CD / Promote to Int (push) Successful in 10s
Prisma models `pages`/`page_updates`/`page_content_cache` per data-model.md. Endpoints: POST /ponds/:id/pages (title -> empty Yjs doc state, seeded via y-prosemirror), GET /pages/:id (meta + base64 state), PUT /pages/:id/state (client-encoded Yjs state, rejected above the 5 MiB operations.md limit or if it doesn't decode into a valid document for the schema), PATCH /pages/:id (title/slug — explicit slug changes validate uniqueness per pond, title-only renames keep the slug), DELETE (soft). Access follows InterimAccessService via the page's pond, same 404-not-403 interim rule as ponds. State saves decode the Yjs update with yjs + y-prosemirror and run it through the #24 shared derivation functions (docToPlainText/ docToMarkdown/docToHtml/extractOutline) to refresh page_content_cache. The Yjs XmlFragment name ("default") and the derivation call are factored so the collab server's persistence hooks (#35) can reuse both. Raised the API's JSON body limit to 8 MiB (main.ts and the e2e test app) to fit base64-encoded page state. Closes #23 |