Commit Graph

6 Commits

Author SHA1 Message Date
e54aaf76f9 Add the comments panel, unread badge, and comment-policy setting (#92)
Some checks failed
CI / Lint, typecheck, test (push) Successful in 3m21s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m43s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 10s
CI / Auth e2e pack (push) Failing after 3m1s
CI / Import/export fidelity gate (push) Has been skipped
New comments panel on the page (toggle next to attachments, unread badge
counting comments newer than the last localStorage-recorded visit):
threaded display with relative times and author names, a Markdown
composer with hints, edit/delete for authors, resolve moving threads
into a collapsed resolved <details> section with reopen, and a
permission-aware composer — hidden with a hint when the pond's policy
bars the viewer (readers always see the discussion). The pond settings
page gains the "who may comment" select. Fixes PondsService.update
silently dropping commentPolicy from the settings merge (found by the
new two-user Playwright pack; the DB test now exercises the real pond
PATCH). New comments i18n namespace (de+en); the pack runs as its own
CI step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 22:07:16 +02:00
f500198c5d Add per-pond fonts: catalog, build, application, and admin UI (#66)
All checks were successful
CD / Build and push images (push) Successful in 3m24s
CI / Lint, typecheck, test (push) Successful in 3m6s
CI / Auth e2e pack (push) Successful in 4m8s
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
Self-hosted Google Fonts with per-pond selection (ADR 0016), the GDPR
"zero external requests" posture (security.md, CSP `font-src 'self'`).

- Catalog: a curated 15-family OFL/Apache list in shared (family, weights,
  category, license, google-webfonts-helper id). `deploy/fonts/build-fonts.mjs`
  validates every entry has license info (fails the build otherwise),
  downloads the WOFF2 weights into apps/web/public/fonts/ (gitignored), and
  generates the @font-face stylesheet — run at image build time from the web
  Dockerfile (with retries), never from a visitor's browser.
- Application: PondFontScope sets --font-heading/body/mono (+ weights) from
  pond.settings.fonts on the editor + read view; the existing global CSS
  already reads those custom properties, so headings/body/code re-resolve to
  the pond's fonts. A pond with no settings arrives with the defaulted values
  (Roboto 400 / Roboto 200 / Fira Code), so the vision defaults always render.
- Admin UI: pond-settings 'Appearance' section — three slots (family + weight)
  with a live preview, Pond-Admin-gated (fonts added to updatePondInputSchema
  and merged in PondsService.update); a font catalog attribution page (/fonts)
  listing families and licenses. New `font` i18n namespace (de+en).
- CSP: strict Content-Security-Policy in nginx.conf (default-src 'self';
  font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; …) —
  the app's scripts are all external files, inline styles cover CSS variables.
- Tests: shared catalog-integrity unit test (the invariant the build enforces);
  e2e fonts pack — no request leaves the origin when rendering a pond (the GDPR
  network assertion), a font choice applies to a page and persists, and a pond
  without settings renders the defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-10 11:16:29 +02:00
0c6494f209 Enforce permissions in API guards and retire interim access (#52)
All checks were successful
CD / Build and push images (push) Successful in 2m54s
CI / Lint, typecheck, test (push) Successful in 2m25s
CI / Auth e2e pack (push) Successful in 2m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 12s
Every route now declares its access rule explicitly and is enforced
through the shared resolution algorithm (permissions.md):

- PermissionGuard + decorators (@RequiresPondRole, @RequiresPagePermission,
  @RequiresAttachmentPermission, @AuthenticatedOnly) applied to every
  route; a route-enumeration test proves full coverage alongside
  @Public()/Site-Admin-guarded routes.
- 404/403 policy (documented in README conventions): denied reads answer
  404 (existence hiding), denied writes on readable things answer 403;
  trash views need write capability (ADR 0013).
- PermissionService resolves page/pond questions via the shared resolver,
  with an in-process pond-context cache (grants + label parents) that is
  invalidated on every grant/label-tree change and TTL-bounded as a
  multi-process safety net. Grant changes also fire pond_access_changed
  for collab revalidation (#39/#53).
- shared: pond-scope resolution (hasPondRole, canSeePond) next to the
  page resolver; grant wire schemas + GrantView.
- Owner Pond-Admin grants: migration backfill for all existing ponds,
  created transactionally with every new pond (shared + personal + seed).
- Grant CRUD under /ponds/:id/grants (pond_admin-gated) with structural
  and referential validation, last-admin protection, audit logs.
- InterimAccessService deleted; page lists, search, backlinks, phantom
  links, and trash listings are filtered per page through the resolver;
  collab tokens are now truly ro for readers.
- Fixture-matrix e2e (reader/editor/pond admin/foreign, label-deny,
  authenticated-subject, revoke-then-immediate-deny cache test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 16:31:41 +02:00
fa7ae033b5 Add permission-revocation handling for live and offline sessions (#39)
All checks were successful
CD / Build and push images (push) Successful in 2m58s
CI / Lint, typecheck, test (push) Successful in 1m55s
CI / Auth e2e pack (push) Successful in 2m25s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 11s
Revoking write access must terminate live sessions and let a user with
pending offline edits export them rather than lose them silently.

Backend (generic, reused by M5 grants #53):
- packages/shared: POND_ACCESS_CHANGED_CHANNEL, the LISTEN/NOTIFY channel
  shared by api and collab.
- api: PondAccessNotifier emits pg_notify(pond_access_changed, pondId) on
  a permission-relevant change; the single generic seam for revocation.
  Wired into pond soft-delete as the interim trigger (see==modify until
  #53).
- collab: a dedicated-connection LISTEN listener (LISTEN is connection-
  bound, not pooled) that, on a notification, closes every open connection
  to the pond's open pages. Clients then reconnect and the api re-issues a
  token reflecting current access (downgrade to ro, or 403/404). Reconnects
  and re-LISTENs if its connection drops.

Frontend:
- use-collab-provider: a refused token (403/404) on (re)connect sets
  accessRevoked and stops the reconnect loop; exposes discardLocal.
- AccessRevokedDialog: keeps local content visible and offers Markdown
  copy/download (derived from the live editor doc, so offline edits are
  included) and an explicit discard that clears IndexedDB. de+en strings.

Tests: collab DB-backed integration test proves a direct NOTIFY closes a
live session within seconds (AC1) and leaves unrelated ponds untouched;
listener unit tests; api test asserts soft-delete fires the notifier;
web test for the export Markdown derivation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 07:01:46 +02:00
64928f0ac0 Quota foundation: overrides, resolution, race-safe consumption (#22)
All checks were successful
CD / Build and push images (push) Successful in 1m46s
CI / Lint, typecheck, test (push) Successful in 1m17s
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
- quota_overrides + pond_usage models (BigInt values, unique per
  subject+key); migration 20260705185146_quotas
- instance-default quota keys in the settings registry (editors 5,
  readers 50, additional ponds 0, storage 1 GiB, max file 25 MiB)
- QuotaService: getEffective with pond → user → instance resolution
  (zero counts as a value, not a gap); assertCanCreateSharedPond and
  checkAndConsume serialize via pg_advisory_xact_lock inside the guarded
  write's transaction; release never drops below zero
- pond creation enforces additional_ponds (personal ponds don't count);
  quota errors carry code quota_exceeded + {quotaKey, limit}, localized
- seed grants fixtures an additional_ponds override (default is 0)
- table-driven resolution tests, parallel-consumption test, e2e for the
  pond-creation limit

Closes #22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpQz6ypHJsLfMf4S6fyQEB
2026-07-05 20:55:59 +02:00
f0850eecd3 Ponds: data model, CRUD API, personal pond on verification (#21)
All checks were successful
CD / Build and push images (push) Successful in 1m46s
CI / Lint, typecheck, test (push) Successful in 1m19s
CI / Auth e2e pack (push) Successful in 1m42s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m5s
CD / Promote to Int (push) Successful in 10s
- Pond model with pond-level trash columns (ADR 0013) and settings jsonb
  holding only deviations from the defaults (sidebar sort, font slots per
  ADR 0016); migration 20260705090100_ponds
- shared: pond schemas/views and slugify (German transliteration,
  URL-safe, length-capped); deterministic -2/-3 suffixes for collisions
- InterimAccessService: single place answering pond access questions
  until the real role model lands in M5
- POST/GET /ponds, GET /ponds/:slug, PATCH/DELETE /ponds/:id, Site-Admin
  trash + restore; personal pond auto-created on e-mail verification and
  for active seed fixtures; personal ponds cannot be trashed
- e2e pack covering verify-flow pond creation, slug suffixes, rename,
  foreign-pond 404s, trash/restore; slugify unit tests

Closes #21

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpQz6ypHJsLfMf4S6fyQEB
2026-07-05 11:08:16 +02:00