A search palette over the #49 full-text search.
- web:
- `SearchPalette`: opened from a top-bar button or the global "/" shortcut
(ignored while typing in a field). Scoped to the current pond by default
with an "all my ponds" toggle and, when scoped, a label filter. Results
list title, pond, label chips, and a highlighted snippet; recent searches
(localStorage) show before typing; empty/error/hint states.
- `HighlightedSnippet` renders the match — the api wraps hits in shared
sentinels (private-use codepoints), split here into `<mark>` so no HTML
from the content is interpreted.
- Fully keyboard-operable: "/" opens, ↑/↓ move, Enter opens the page,
Esc closes.
- i18n `search` namespace (de + en); palette + result styles.
- api: a pondId scope test proves search narrows to one pond.
- e2e `search.spec.ts` (new CI pack): body content added via the editor is
found and highlighted, the scope toggle keeps the result, and Enter opens
the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
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
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
Backend: a generic maintenance-job scheduler (SchedulerService, `jobs`
table) that any later maintenance job registers with instead of
growing its own timer loop. Due-ness and the run-mutex both live in
the DB row (`lastRunAt` survives a restart; claiming a due job is one
atomic `UPDATE ... WHERE status != 'RUNNING'`), and an injectable
ClockService lets tests simulate retention elapsing without waiting or
faking the global clock.
Trash endpoints: GET /ponds/:id/trash (list), POST /pages/:id/restore,
DELETE /pages/:id/purge (manual, bypasses retention) — all sharing the
same purge logic as the scheduled daily job (default 30-day retention,
new trash.retentionDays instance setting). Purging deletes a page's
content cache, update log, and attachment files/quota; page_versions
is a placeholder until M3 exists. Direct navigation to a trashed page
now 404s with a distinguishable `page_trashed` code for editors (a
plain 404 for everyone else) instead of the generic not-found.
Attachment.pageId — added in #27 but never wired up — now gets set on
every page state save to whichever page's document currently embeds
the file, which is what lets purge find a page's files.
Frontend: a per-pond trash view (restore/purge), a "move to trash"
action with confirmation in the page menu, and a trash link in the
sidebar for pond owners. Also fixes react-query retrying 4xx responses
for several seconds by default, which was masking the trash-hint 404
in the UI (and would have affected any other not-found/permission
error the same way).
Closes#31
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
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>
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>