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
Bootstrap apps/collab as a Hocuspocus WebSocket server (ADR 0003):
- pino JSON logging (service=collab) and shared Zod env validation
(collabEnvSchema); structured connection open/close logs.
- /healthz endpoint (process liveness + PostgreSQL ping) served via the
onRequest hook, matching the container-internal path and the proxied
/collab/healthz path; any WebSocket handshake is accepted for now
(authentication arrives with #34, persistence with #35).
- Dockerfile (ESM workspace build) and a compose service on the frontend
and internal networks with a healthcheck; dev overlay service and a new
COLLAB_PORT variable.
- CD builds, pushes, and promotes the collab image; CI builds it on PRs;
the smoke suite asserts /collab/healthz through the reverse proxy.
- deployment.md/stages.md: proxy routing, per-stage COLLAB_PORT, checklist.
Closes#33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit's "Auth e2e pack" job failed: auth.spec.ts's own
logins already spend a good chunk of the 10/min/IP login rate limit
(operations.md), leaving content.spec.ts's five more logins to hit it
mid-pack. Reproduced locally against a fresh throwaway database
(migrate + seed + both Playwright runs back to back, exactly the job's
steps) and confirmed a DELETE on rate_limits between the two runs
fixes it.
Follow-up to #32, not a new issue — the pack itself was already
correct, this is a test-infrastructure fix.
Seed script extends the fixture matrix with a shared "Content Fixtures"
pond (owned by fixture-user): an "Every Element" page covering every
editor schema node and mark (#24), and a "Fixture Image" page with one
real, servable uploaded image. "Every Element" loads a checked-in Yjs
snapshot (prisma/fixtures/content-page.yjs) generated from a
human-readable Markdown source (content-page.md) via a deterministic
regeneration script (pinned Y.Doc clientID; refuses to write a
snapshot that isn't a fixed point of the Markdown round-trip).
New apps/web/e2e/content.spec.ts consolidates the M2 content
regression pack: page lifecycle, editor basics, image paste, trash,
and — the pack's actual regression pin — a byte-for-byte comparison of
the fixture page's exported Markdown against the checked-in fixture.
Verified this catches regressions: temporarily mutated
docToMarkdown's heading serializer, rebuilt, re-seeded, confirmed the
comparison failed, then reverted.
This pack now runs in CI (a second step in the existing auth-e2e job,
reusing its already-built-and-seeded stack) alongside the existing
local-only feature packs.
Closes#32
The Vite dev server died mid-run on the CI runner (memory pressure),
failing every remaining test with connection refused. The auth-e2e
job now serves apps/web/dist through a dependency-free static server
with SPA fallback and /api proxy (scripts/e2e-static-server.mjs) —
matching the production nginx/Caddy layout and testing the real
build. Server logs are dumped when the job fails. Verified locally:
six of six against the static server.
Part of #20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playwright's request context resolves localhost to ::1 while Vite in
the CI container listened on IPv4 only — the fixture-login helper got
ECONNREFUSED. Vite now starts with --host in the auth-e2e job. The
redirect test also reports the server's error message instead of a
bare URL mismatch when a login fails.
Part of #20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixtures seed ran against an empty database; migrate deploy now
precedes it (the api start re-checks idempotently).
Part of #20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seed script now provisions the documented fixture matrix
(fixture-admin / fixture-user / fixture-pending, idempotent upserts,
rate-limit reset for disposable databases). A six-test Playwright pack
drives the real UI against a full local stack with Mailpit: complete
signup→mail→verify→first-login journey, wrong-password error, guarded
route redirect honoring ?next (race between the login page and the
anonymous guard fixed by teaching the guard about ?next), menu logout,
site-admin gating, and a profile rename reflected in the top bar. The
pack self-skips without E2E_MAILPIT_URL, so the CD smoke stage (now
pinned to smoke.spec.ts) stays untouched; a new CI job boots api +
web dev server against postgres/mailpit service containers and runs
the pack on every PR and push. Also fixed: the web api client choked
on empty 201 bodies. e2e/README.md documents targets and fixtures.
Closes#20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prisma models per data-model.md: users (status enum, site-admin flag),
user_identities (password provider now, OIDC later — subject is the
stable user id), sessions (hashed ids), auth_tokens (hashed, single-
use), plus rate_limits and mail_outbox for the upcoming M1 stories.
UsersService creates accounts transactionally with Argon2id-hashed
password identities (OWASP parameters, rehash detection) and maps
uniqueness violations to field-level conflicts. Database-backed suites
run when TEST_DATABASE_URL is set — locally against the dev db, in CI
via a new postgres service container; shared auth schemas (username,
password policy incl. common-password blocklist) ship with tests.
Closes#10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm/action-setup reads the version from package.json packageManager —
the explicit `version: 11` input made it fail on the mismatch. The
registry login now strips whitespace from the stored token before
docker login (the secret carried a trailing newline).
Part of #8
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>
Gitea Actions workflow running on every PR and push to main: pnpm
install with caching, workspace build, ESLint+Prettier, tsc, Vitest,
translation key parity, and docker builds of both images (build-only —
pushing is the CD workflow's job). Live verification of the red/green
PR gate follows once the act_runner from issue #9 is registered;
issue #7 stays open until then.
Part of #7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>