Commit Graph

6 Commits

Author SHA1 Message Date
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
2026-07-12 07:13:34 +02:00
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
2026-07-09 12:18:44 +02:00
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
2026-07-09 11:41:42 +02:00
1bda137ca4 Add version history UI: list, view, diff, restore (#42)
All checks were successful
CD / Build and push images (push) Successful in 2m54s
CI / Lint, typecheck, test (push) Successful in 2m3s
CI / Auth e2e pack (push) Successful in 2m41s
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 12s
Users can see who changed what and restore old states (ADR 0013).

- shared: dependency-free word-level Markdown diff (diffMarkdown) with a
  unit test; PageVersionContentView; PAGE_RESTORE_CHANNEL + PageRestoreRequest.
- api: GET /pages/:id/versions (list), GET .../:versionId (read-only HTML +
  Markdown for diffing), POST .../:versionId/restore. Every route requires
  write access — viewing history is gated like editing (permissions.md).
  Restore checks permission, then emits the page_restore NOTIFY; history is
  append-only (the api never deletes a version).
- collab: a page_restore listener applies the restore on the live document via
  openDirectConnection — it snapshots the current state as a PRE_RESTORE
  version, then replaces the content in one transaction, so every connected
  client converges and the change persists like a normal edit.
- web: HistoryPanel (version list with time/trigger/label/contributors, a
  read-only render of a selected version, a Markdown diff against the current
  page, and a restore action), toggled from the page menu. de+en strings.

Tests: shared diff (added/removed/round-trip/edges); collab restore DB test
(a connected client converges on the restored content; a pre-restore snapshot
is appended alongside the original — append-only); api list/get/restore
(newest-first, rendered content, write-permission gate, restore returns the
target without mutating history).

This completes M3 (real-time collaboration & history, #33–#42).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 08:53:42 +02:00
6fb6f6fce7 Add version snapshots: automatic, named, thinning (#41)
All checks were successful
CD / Build and push images (push) Successful in 2m53s
CI / Lint, typecheck, test (push) Successful in 2m1s
CI / Auth e2e pack (push) Successful in 2m24s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Version history is a core kickoff decision (ADR 0013). Snapshots are full,
self-contained encoded Yjs states, so restore never depends on the update
log and compaction (#40) cannot lose history.

(The page_versions / page_pending_contributors tables and base schema
landed a commit early, bundled into 3583a04; this commit completes #41.)

- schema: page_versions gains created_by (editor of manual/pre-restore
  versions; null for automatic snapshots). shared: PageVersionView,
  CreateVersionInput, PageVersionTrigger.
- collab: PostgresVersionStore tracks contributors per open doc (onChange),
  flushes them to the shared page_pending_contributors accumulator on store,
  creates an automatic snapshot on last-participant disconnect (only if
  something changed — no duplicate on a quick reconnect) and every 30
  active-editing minutes. Contributors and snapshot are consumed atomically.
- api: POST /pages/:id/versions creates a named version (write permission,
  label + creator, snapshot reconstructed from persisted state, consumes the
  same contributor accumulator). Daily version-thinning scheduler job keeps
  all versions for 90 days, then the newest auto snapshot per day; manual and
  pre-restore versions are never thinned. pre_restore trigger reserved for #42.

Tests: collab (one auto version on session end with the full two-author
contributor set, none when unchanged, no duplicate on reconnect, interval
snapshot); api (named version stores label+creator, contributor set consumed,
non-owner refused, thinning time-travel keeps newest-per-day beyond window).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 08:37:07 +02:00
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
2026-07-05 22:25:41 +02:00