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
Implements the FileStorage abstraction (uploads/<pondId>/<fileId> on the
mounted volume), the attachments model, and POST /ponds/:id/files, GET
/media/:fileId, DELETE /files/:id. Uploads are validated by sniffing
magic bytes rather than trusting the client's Content-Type/filename
(catches a renamed .html-as-.png), checked against the max_file_bytes
and storage_bytes quotas, and served with nosniff + immutable caching.
Closes#27
React StrictMode double-invokes effects in development; the second
POST consumed-token 400 could win the state race and show an error
for a successful verification (flaked in CI, passed locally). A ref
guards the single-use call; Playwright test-results are ignored.
Part of #20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pnpm cache used by the CI workflow lives in .pnpm-store/ inside
the workspace; Prettier and ESLint must not descend into it.
Part of #8
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>