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>