Commit Graph

65 Commits

Author SHA1 Message Date
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
af81b50fa6 Add offline editing: local persistence, PWA shell, offline resolution (#38)
Some checks failed
CD / Build and push images (push) Successful in 2m59s
CI / Lint, typecheck, test (push) Successful in 2m3s
CI / Auth e2e pack (push) Failing after 2m18s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m32s
CD / Promote to Int (push) Successful in 12s
Editing continues without a connection and merges conflict-free on
reconnect (ADR 0003, realtime-collaboration.md §Offline).

- y-indexeddb mirrors every opened page's Y.Doc to IndexedDB, sharing the
  document with the collab provider. The local copy is discarded when the
  page is left after a successful server sync (bounding IndexedDB growth)
  and kept otherwise so offline edits survive to the next visit.
- vite-plugin-pwa service worker precaches the app shell (build assets only)
  with a navigation fallback; `/api` and `/collab` are denylisted and there
  is no runtime caching, so API responses are never cached or poisoned.
- Offline page resolution WITHOUT caching API responses: the app itself
  persists the small metadata it needs to reopen a visited page (page/pond
  ids + slugs, bounded LRU in localStorage) and the last signed-in user, so
  after an offline tab reload the app stays signed in, resolves the page, and
  restores its content from IndexedDB. Both are revalidated when the network
  returns (a 401 clears the cached user).
- Local-only UI: a banner when there are edits held only on this device
  (provider `onUnsyncedChanges`), de + en.

Tests: `page-cache` unit test (remember/recall + bounded eviction); a new
`offline` e2e pack (validated locally against the full stack and wired into
CI): edit, reload while offline (shell from the SW, content from IndexedDB),
assert an API call fails offline (no SW API caching), then reconnect and a
second client converges. The e2e static server serves `.webmanifest`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-08 22:08:55 +02:00
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
2026-07-08 18:00:19 +02:00
8316c617d2 Add collaboration server skeleton (Hocuspocus) with health, container, and CI/CD (#33)
All checks were successful
CD / Build and push images (push) Successful in 2m36s
CI / Lint, typecheck, test (push) Successful in 1m50s
CI / Auth e2e pack (push) Successful in 1m58s
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
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>
2026-07-08 14:53:44 +02:00
49fcbc45a6 CI: reset login rate limit between the auth and content e2e packs
All checks were successful
CD / Build and push images (push) Successful in 48s
CI / Lint, typecheck, test (push) Successful in 1m43s
CI / Auth e2e pack (push) Successful in 1m57s
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
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.
2026-07-08 13:39:33 +02:00
12035e2231 Add M2 fixtures and content regression pack (#32)
Some checks failed
CD / Build and push images (push) Successful in 2m2s
CI / Lint, typecheck, test (push) Successful in 1m44s
CI / Auth e2e pack (push) Failing after 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 1m8s
CD / Promote to Int (push) Successful in 10s
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
2026-07-08 13:14:48 +02:00
d05c36701b Run the CI auth pack against the production build
All checks were successful
CD / Build and push images (push) Successful in 40s
CI / Lint, typecheck, test (push) Successful in 1m14s
CI / Auth e2e pack (push) Successful in 1m39s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 10s
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>
2026-07-05 06:16:27 +02:00
d29e95462c Bind the CI dev server to all interfaces and surface login errors
Some checks failed
CD / Build and push images (push) Successful in 46s
CI / Lint, typecheck, test (push) Successful in 1m14s
CI / Auth e2e pack (push) Failing after 1m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m2s
CD / Promote to Int (push) Successful in 10s
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>
2026-07-05 06:07:49 +02:00
6e2d513cc7 Apply migrations before seeding in the auth-e2e job
Some checks failed
CD / Build and push images (push) Successful in 38s
CI / Lint, typecheck, test (push) Successful in 1m22s
CI / Auth e2e pack (push) Failing after 1m55s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m4s
CD / Promote to Int (push) Successful in 9s
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>
2026-07-05 05:52:49 +02:00
1cea675983 Add auth e2e regression pack with fixtures and CI stack
Some checks failed
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Successful in 1m41s
CI / Lint, typecheck, test (push) Failing after 56s
CI / Auth e2e pack (push) Failing after 43s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Has been cancelled
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>
2026-07-05 05:43:05 +02:00
36608177f6 Add user, identity, session, and auth-support data model
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>
2026-07-05 00:42:22 +02:00
8c3839c2bc Fix pipeline: pnpm version source and registry login whitespace
Some checks failed
CD / Build and push images (push) Successful in 1m41s
CI / Lint, typecheck, test (push) Failing after 1m40s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 21s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 19s
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>
2026-07-05 00:17:48 +02:00
fb1422f56f Add CD workflow: build-push, deploy Test, smoke suite, promote Int
Some checks failed
CD / Build and push images (push) Failing after 5s
CD / Deploy to Test (push) Has been skipped
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Failing after 7s
CI / Build container images (push) Has been skipped
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>
2026-07-04 19:57:16 +02:00
9aa7b14349 Add CI workflow: lint, typecheck, tests, i18n check, image builds
Some checks are pending
CI / Lint, typecheck, test (push) Waiting to run
CI / Build container images (push) Waiting to run
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>
2026-07-04 19:31:17 +02:00