Commit Graph

110 Commits

Author SHA1 Message Date
Marcel
58922bee53 docs(ci): add Troubleshooting section for Reload Caddy failures
Covers the three failure modes Sara flagged: Caddy stopped (explicit
systemctl error), symlink missing/mis-pointed (silent reload, stale
smoke test), and Docker socket / nsenter unavailable (container error).
Each failure mode includes symptoms and recovery steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
bbdf1c3e67 docs(adr): ADR-012 — nsenter via privileged container for host service management in CI
Captures the architectural decision, alternatives considered (sudo
systemctl, Caddy admin API, SSH), and consequences (symlink contract,
Renovate review requirement, step duplication tracked in #539).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
8536b2ebbd docs(deploy): note Caddyfile symlink is a CI dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
4bb988824f docs(ci): update nsenter example to Alpine, document alternatives considered
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
fe2cdaae83 docs(ci): document DooD runner architecture and nsenter pattern
Replace the stale generic runner provisioning docs with an accurate
description of the actual two-container setup on the Hetzner VPS.
Document the nsenter pattern for running host-level commands (systemctl)
from containerised CI steps, and the Caddyfile symlink contract that the
reload step depends on.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 07:42:28 +02:00
Marcel
a40267e490 docs(deployment): document IMPORT_HOST_DIR and mass-import workflow
DEPLOYMENT.md line 81 declares any compose env var missing from §2 a
blocking review comment. IMPORT_HOST_DIR (added on this branch) was
unmentioned. Adds the row and rewrites §6.4 so the staging/prod operator
workflow (rsync host → set env → trigger import) is in the runbook,
not just buried in compose comments.

Addresses review feedback from Markus and Tobias on #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 20:05:14 +02:00
Marcel
4d4d5793bb docs(glossary): add archiv-app service account entry
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m48s
CI / OCR Service Tests (pull_request) Successful in 16s
CI / Backend Unit Tests (pull_request) Successful in 4m5s
CI / fail2ban Regex (pull_request) Successful in 37s
CI / Compose Bucket Idempotency (pull_request) Failing after 50s
CI / Unit & Component Tests (push) Failing after 2m46s
CI / OCR Service Tests (push) Successful in 15s
CI / Backend Unit Tests (push) Successful in 4m4s
CI / fail2ban Regex (push) Successful in 37s
CI / Compose Bucket Idempotency (push) Failing after 50s
`archiv-app` is the bucket-scoped MinIO service account introduced
in PR #499 alongside the production deploy pipeline. Until now the
term only appeared in `infra/minio/bootstrap.sh` and the prod compose
file; a reader encountering `S3_ACCESS_KEY: archiv-app` had no
single-page reference distinguishing it from the MinIO root account.

Adds a new "Infrastructure Terms" section to docs/GLOSSARY.md so the
distinction (root account vs. application service account) and the
attached `archiv-app-policy` scope live in the canonical glossary
location. Cross-links to ADR-010 for the MinIO-stays-self-hosted
rationale. Addresses @elicit's round-2 recommendation on PR #499.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:11:46 +02:00
Marcel
9adde3cd89 refactor(compose): rename docker network archive-net to archiv-net
The docker network was the only `archive-*` identifier in either
compose file; everything else (user, db, bucket, service account,
project name) uses the `archiv-*` spelling. Reviewers' eyes stuttered
on it on the prod compose review (round 2 of PR #499 — Markus and
Tobi). Renamed in both prod and dev compose for consistency and
updated the single doc reference to the dev-project-prefixed
network name.

Operational note: applying this change to a running stack will
recreate the network on the next `docker compose up`; containers
restart, named volumes are unaffected.

`docker compose config --quiet` passes for both compose files and
for the staging profile. Sweep confirms zero `archive-net`
references remain in the tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:10:39 +02:00
Marcel
a7a80f8c16 docs(deployment): route SSE through Caddy in topology mermaid
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m48s
CI / OCR Service Tests (push) Successful in 16s
CI / Unit & Component Tests (pull_request) Failing after 2m48s
CI / Backend Unit Tests (pull_request) Successful in 4m8s
CI / fail2ban Regex (pull_request) Successful in 37s
CI / Compose Bucket Idempotency (pull_request) Failing after 49s
CI / Backend Unit Tests (push) Successful in 4m7s
CI / fail2ban Regex (push) Successful in 36s
CI / Compose Bucket Idempotency (push) Failing after 1m15s
CI / OCR Service Tests (pull_request) Successful in 16s
The top-level deployment diagram lagged the C4 L2 diagram, which
correctly notes that SSE notifications are fronted by Caddy. The
mermaid showed Browser → Backend direct, which would only be true
if the backend port were exposed publicly (it is not — all docker
ports bind to 127.0.0.1).

Fixes the inconsistency Markus flagged on PR #499: the public
surface is Caddy and Caddy only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:18:11 +02:00
Marcel
03d478840b docs(arch): show Caddy + X-Forwarded-Proto in auth-flow diagram
Adds the Caddy hop to seq-auth-flow.puml and surfaces the two
production-relevant header behaviours:

  - Caddy terminates TLS and forwards X-Forwarded-Proto: https
  - Spring Boot trusts this header (server.forward-headers-strategy:
    native, ForwardedRequestCustomizer at the Jetty layer), so
    request.getScheme() returns "https"
  - The Set-Cookie response carries the Secure flag because the
    observed scheme is https — without forward-headers-strategy this
    would silently drop to plain http and the cookie would lose Secure

Closes the doc-currency gap flagged in the Markus review on PR #499:
"Auth flow change → docs/architecture/c4/seq-auth-flow.puml".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:17:12 +02:00
Marcel
6a6a1c4353 docs(adr): ADR-011 single-tenant Gitea runner with on-disk env-files
Records the operational assumption that nightly.yml and release.yml
bake in: the self-hosted runner is single-tenant, so writing secrets
to .env.staging / .env.production on disk and removing them via an
`if: always()` cleanup step is acceptable for v1.

Documents the three migration triggers (second repo on the runner,
untrusted PR execution, move to shared infrastructure) and the
one-step migration path (--env-file <(printf '%s' "$SECRET_BLOB"))
so the next operator does not silently break the trust assumption.

The in-comment notes at the top of both workflow files already point
at this ADR's content; this commit records the decision in the durable
location the doc-currency table demands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:16:20 +02:00
Marcel
b57afb9ad2 docs(adr): ADR-010 MinIO stays self-hosted, Hetzner OBS deferred
Records the reversal of the earlier "migrate to Hetzner Object Storage"
direction in docs/infrastructure/production-compose.md. Documents the
cost/benefit (current 13 GB fits trivially on the VPS; OBS billing is
dominated by base fee at this size; migration is a three-env-var swap
plus `mc mirror`, no application rewrite cost).

Captures the four triggers that should re-open the decision (50 GB
threshold, healthcheck latency, VPS upgrade cost, backup runtime) so
the deferral does not become an indefinite punt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:15:38 +02:00
Marcel
59bc81d353 docs(adr): ADR-009 standalone docker-compose.prod.yml, not overlay
Records the decision to make docker-compose.prod.yml a fully self-contained
file rather than an overlay over docker-compose.yml. Captures the cost
(env-var duplication across dev and prod files) and the benefit (single
file the reviewer can hold in their head, no Compose merge-rule
surprises, automatic project-name namespacing for cohabiting staging +
production on one host).

Surfaces the retirement of the earlier overlay narrative in
docs/infrastructure/production-compose.md so a future maintainer does
not reverse the choice out of ignorance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:14:58 +02:00
Marcel
ba5bd9cb11 docs(deployment): document fail2ban symlink, OCR_MEM_LIMIT, smoke test
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m53s
CI / OCR Service Tests (push) Failing after 1m58s
CI / Backend Unit Tests (push) Failing after 1m23s
CI / Unit & Component Tests (pull_request) Failing after 3m59s
CI / Backend Unit Tests (pull_request) Successful in 5m39s
CI / OCR Service Tests (pull_request) Successful in 1m14s
Updates DEPLOYMENT.md to match the infra changes in this PR:

§1 OCR memory — point operators at the new OCR_MEM_LIMIT env var instead
                of telling them to edit "the prod overlay".
§2 OCR env vars — add OCR_MEM_LIMIT to the table.
§3.1 server setup — replace fail2ban prose with concrete `ln -sf`
                    commands referencing the committed jail/filter.
                    Document the single-tenant runner assumption near
                    the runner-registration step.
§3.4 first deploy — describe the new automated smoke test step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 12:07:59 +02:00
Marcel
e4df17f308 docs: retire overlay narrative; add Caddy to C4 L2 diagram
Some checks failed
CI / Unit & Component Tests (push) Failing after 7m31s
CI / OCR Service Tests (push) Successful in 49s
CI / Backend Unit Tests (push) Failing after 3m30s
CI / Unit & Component Tests (pull_request) Failing after 6m55s
CI / OCR Service Tests (pull_request) Successful in 51s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
- docs/infrastructure/production-compose.md: trimmed to VPS sizing,
  cost breakdown, and Hetzner ecosystem rationale. The inline
  compose spec (overlay + Hetzner OBS in prod) is retired; the
  live file is now docker-compose.prod.yml at the repo root and
  the Caddyfile lives at infra/caddy/Caddyfile. Observability
  stack is called out as a not-yet-deployed gap (issue #498).

- docs/architecture/c4/l2-containers.puml: adds Caddy as a named
  reverse-proxy container with the two port paths and notes the
  archiv-app service-account split on MinIO access.

Refs #497.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:00:21 +02:00
Marcel
2eade2b78f docs(deployment): rewrite for Gitea Actions / Caddy / prod compose
Brings DEPLOYMENT.md in line with the production deployment landed
in #497:

- Topology diagram: frontend port 3000 (Node adapter), 127.0.0.1
  binding, project-name isolation between prod and staging
- Caddyfile now lives in-tree at infra/caddy/Caddyfile (symlinked
  onto the server)
- Dev vs prod table: documents the new deploy method (workflows +
  --wait) and the prod-compose specific differences
- Env vars: adds MINIO_APP_PASSWORD; notes that prod compose
  hardcodes the MinIO root user and the bucket name
- Bootstrap section: server hardening, fail2ban, Tailscale, the 16
  Gitea secrets, and the workflow_dispatch first-deploy step
- Admin password warning: first deploy locks the password, secret
  rotation after that point has no effect
- Rollback: TAG= override + docker compose up -d --wait

Refs #497.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 21:58:51 +02:00
Marcel
a364e3f69b docs(adr): ADR-008 SQL-level FTS pagination via window-function CTE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:35:01 +02:00
Marcel
a072701632 docs(adr): ADR-007 reader-dashboard permission discriminant
Some checks failed
CI / Unit & Component Tests (push) Failing after 6m33s
CI / OCR Service Tests (push) Successful in 1m7s
CI / Backend Unit Tests (push) Failing after 4m31s
Captures the architectural decision behind isReader = !canWrite &&
!canAnnotate, why BLOG_WRITE intentionally lands on the reader
dashboard, the alternatives considered (separate route, AppUser
column, middleware redirect, BLOG_WRITE exclusion), and the
implications for future permission additions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:56:47 +02:00
Marcel
9e1754bbb0 docs: add Reader glossary entry + clarifying comments on specs and query
- GLOSSARY.md: defines "Reader" as the permission-derived role
  (isReader = !canWrite && !canAnnotate) — addresses @Markus blocker
- GeschichteSpecifications.hasAuthor: comment explains null = no restriction
  (PUBLISHED path) — addresses @Markus suggestion
- PersonRepository.findTopByDocumentCount: comment explains alias-in-ORDER-BY
  is intentional PostgreSQL behaviour — addresses @Markus suggestion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:56:47 +02:00
Marcel
301cfffd1a docs(c4): align density breakpoint with code (≥1024px) (#385)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m4s
CI / OCR Service Tests (pull_request) Successful in 36s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
CI / Unit & Component Tests (push) Failing after 4m3s
CI / OCR Service Tests (push) Successful in 40s
CI / Backend Unit Tests (push) Failing after 3m22s
The widget hides below the Tailwind lg breakpoint to protect the
44×44 touch-target floor on tablet (Leonie's round-1 finding) but
the diagram still claimed 640px (sm). Update both the docsListPageTs
description, the timelineFilter description, and the relationship
label to match +page.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:07:16 +02:00
Marcel
e8fb8150b7 docs(c4): document timeline density widget across backend+frontend (#385)
- l3-backend-3b: extend DocumentController description to include the
  per-month density aggregation endpoint.
- l3-frontend-3b: add /documents/+page.ts (client-side gated loader) and
  TimelineDensityFilter component, plus relationships to the density
  endpoint and the search dashboard.

Per Markus' follow-up §5: both diagrams are mandatory before merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:17:29 +02:00
Marcel
ba0bfc6a7e docs(db): add Database section to c4-diagrams.md
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m40s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
CI / Unit & Component Tests (push) Failing after 3m52s
CI / OCR Service Tests (push) Successful in 42s
CI / Backend Unit Tests (push) Failing after 3m34s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:44:41 +02:00
Marcel
d4b5c14a26 docs(db): add full ORM diagram (db-orm.puml)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:44:00 +02:00
Marcel
e209d4877d docs(db): add relationship diagram (db-relationships.puml)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:42:46 +02:00
Marcel
66c1998d2f docs(c4): add VS Code PlantUML server config and diagram index
Some checks failed
CI / OCR Service Tests (push) Successful in 58s
CI / Backend Unit Tests (push) Failing after 3m24s
CI / Unit & Component Tests (push) Failing after 12m42s
2026-05-06 22:52:21 +02:00
Marcel
62bef1d267 docs(c4): add L3 frontend 3c/3d and sequence diagrams 2026-05-06 22:52:21 +02:00
Marcel
c3d4762ca0 docs(c4): add L3 frontend 3a middleware/auth and 3b document workflows 2026-05-06 22:52:21 +02:00
Marcel
421d7ffd37 docs(c4): add L3 backend 3e persons, 3f OCR, 3g supporting domains 2026-05-06 22:52:21 +02:00
Marcel
dbf19037fe docs(c4): add L3 backend 3c transcription and 3d users/groups 2026-05-06 22:52:21 +02:00
Marcel
9387fcc17b docs(c4): add L3 backend 3a security and 3b document management 2026-05-06 22:52:21 +02:00
Marcel
264db4e1c9 docs(c4): add L1 context and L2 containers as C4-PlantUML files 2026-05-06 22:52:21 +02:00
Marcel
12f0e21b21 fix(c4): flatten decimal sub-diagram numbering; note invite gate at L1
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m5s
CI / OCR Service Tests (push) Successful in 41s
CI / Backend Unit Tests (push) Failing after 3m33s
- Rename 3b.2→3c, 3c→3d, 3c.2→3e, 3d→3f, 3e→3g to eliminate
  decimal notation that read as version numbers rather than sub-levels
- Update all seven "See diagram X" cross-references to match
- Correct backend intro: "three focused views" → "seven focused sub-diagrams"
- Add "Access by administrator invite." to L1 Family Member description
  to surface the invite-only registration constraint at the context level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
3e33021129 docs(c4): add cross-diagram stub convention note to header
The C4 standard doesn't define this pattern. Adding a one-sentence
explanation so readers unfamiliar with the project's rendering convention
understand what stub components outside System_Boundary blocks mean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
32396c6253 fix(c4): stammbaum — remove D3 library detail from component description
C4 L3 describes responsibility, not library choice. Removing the D3
reference keeps the description implementation-agnostic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
11b4206fe2 fix(c4): sequence diagram — username → email in auth flow
Three stale references: "Enter username + password", Base64 encode
"user:password", and SELECT WHERE username — all updated to email to
match AppUserRepository.findByEmail() and CustomUserDetailsService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
eede9f93a7 fix(c4): loginPage — username → email in component description
CustomUserDetailsService loads by email, not username. The component
description had a stale "encodes username:password" label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
260bb8e164 fix(c4): correct docBulkEdit endpoint /batch → /bulk
DocumentController has @PatchMapping("/bulk"); the component description
had the wrong path. The Rel in the same diagram was already correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
9b82d8e7dd docs(c4): add Email Service to L1 and L2 — NotificationService and PasswordResetService send SMTP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
ab6117c87e docs(c4): fix 3e DashboardService — add documentSvc and transcriptionSvc cross-domain stubs
DashboardService.getResume() calls DocumentService.getDocumentById() and
TranscriptionService.listBlocks() — both missing from the diagram.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
b1f9f1603c docs(c4): add OcrJobRepository intermediary in 3d — route ocrAsync through repo, not bare db
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
f2a901eabf docs(c4): fix 3a secFilter description — BCrypt validation is in DaoAuthenticationProvider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
d6ca0f12c9 docs(c4): fix 3d frontend — add User actor for /hilfe/transkription
The help guide is used by all transcribers, not just administrators. Only
showing admin as the actor was misleading about who accesses this route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
537bfb79f0 docs(c4): fix 3a — remove AOP @Around from secFilter→permAspect rel label
The filter chain doesn't invoke the AOP aspect directly — Spring Security
hands off to the servlet and AOP intercepts at the method level. The label
implied a direct invocation chain that doesn't exist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
f74b586f29 docs(c4): fix 3b frontend — correct docBulkEdit endpoint to /bulk
DocumentController maps the batch update to PATCH /api/documents/bulk,
not /api/documents/batch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
eb464b351a docs(c4): fix 3c.2 — add PersonRelationshipRepository, route through repo
Both RelationshipService and RelationshipInferenceService inject
PersonRelationshipRepository. The previous direct db arrows were inaccurate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
9ad172084a docs(c4): fix 3d OCR — route transcription/annotation through domain services
OcrAsyncRunner injects TranscriptionService and AnnotationService; it only
accesses the DB directly for OcrJob state (OcrJobRepository). The previous
Rel arrow incorrectly showed direct JDBC access for transcription blocks and
annotations, contradicting the component description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
0582edd840 docs(c4): fix service layer relationships in diagrams 3b and 3b.2
Diagram 3b: DocumentService calls PersonService and TagService, not
their repositories directly. Replace personRepo/tagRepo cross-ref
stubs with personSvc/tagSvc to accurately reflect the layering rule.

Diagram 3b.2: TranscriptionService, AnnotationService, and
CommentService each use a JPA repository, not JDBC directly. Add
TranscriptionBlockRepository, AnnotationRepository, and
CommentRepository components and route the service→repo→db chain.
TranscriptionQueueService delegates to DocumentService and
AuditLogQueryService (no repo of its own); replace the incorrect
→db arrow with cross-diagram stubs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
9986af7c3d docs: remove accidentally committed spec file
Spec file was pre-staged from a prior session and bundled into the previous commit. Specs belong in Gitea issues, not committed to the repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
a4bde0953e docs(c4): fix diagram 3c service layer and add missing 3e components
- diagram 3c: GroupController delegates to UserService (not groupRepo directly)
- diagram 3c: add TagService; TagController delegates to TagService (not tagRepo)
- diagram 3e: add DashboardController serving /api/dashboard/resume|pulse|activity
- diagram 3e: add StatsService; StatsController delegates to StatsService

Addresses blocker feedback from Markus, Felix, and Elicit in PR #448 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00
Marcel
1b55588aee docs(c4): rewrite frontend 3b, add 3c people/stories/discovery, add 3d admin/help
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 20:00:07 +02:00