Add Logbuch: project update blog with admin, media and API

Public archive with sidebar navigation, project pages, month archive,
search and entry pages with image blocks. Admin area for brands,
projects, post types, users, api clients, media and the entry editor
with drag and drop images, preview per audience, scheduling and
publish checks. Read and write API with bearer tokens, audience
scoping, idempotent creation, OpenAPI document and editorial guide.
Magic link login with configurable allowed domains, whole app behind
the session gate. 456 tests including design rule checks.
This commit is contained in:
Matthias Giesselmann
2026-07-31 21:33:42 +02:00
commit b90ff252d1
291 changed files with 43671 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# Logbuch Umsetzungsstand
Stand: 30.07.2026
## Plan 1, Fundament: fertig, lokal, nicht in git
| Nachweis | Ergebnis |
|---|---|
| `pnpm test` | 11 Dateien, 112 Tests grün |
| `pnpm typecheck` | ohne Fehler |
| `pnpm build` | erfolgreich, 7 Routen |
| Live gegen `localhost:4700` | Lese-API, gelesen-Status und Sichtbarkeit von Hand geprüft |
| Mutationsprobe an 6 Wächtern | alle 6 werden von Tests gefangen |
Es gibt weiterhin kein Repository. Kein `git init`, kein Commit, bis der Auftraggeber die laufende Anwendung lokal abgenommen hat.
## Abweichungen vom Plan, unterwegs entstanden
| Punkt | Was war | Was gilt |
|---|---|---|
| Postgres-Image | Volume an `/var/lib/postgresql/data` | Postgres 18 verlangt den Mount an `/var/lib/postgresql`, sonst startet der Container nicht |
| TypeScript | 7.0.2 installiert | Next 16.2.12 kommt mit der Compiler-API von TS 7 nicht klar und braucht `experimental.useTypeScriptCli`. Rückweg wäre TypeScript 6 |
| sharp | im Plan nicht erwähnt | Lässt sich auf dem Rechner nicht bauen, aus `onlyBuiltDependencies` entfernt. Wird erst für Bildvarianten in Plan 5 gebraucht und muss dort geklärt werden |
| `tsconfig.json` | im Plan mit Tabs | Next schreibt die Datei beim Start selbst um, sie ist werkzeuggesteuert |
| `findPost` | nur Slug und Zielgruppe | zusätzlich optionale Projektbindung, weil Slugs nur pro Projekt eindeutig sind |
| Token-Abfrage | SQL lag in `src/lib/api-auth.ts` | verschoben nach `src/data/repositories/clients.ts`, damit die eigene Regel gilt: kein SQL außerhalb von `src/data` |
## Behobene Mängel aus dem Review
Fünf Prüfer mit getrennten Blickwinkeln haben 34 Funde gemeldet, 14 wurden gegnerisch geprüft, 9 bestätigt, 5 widerlegt. Vier echte Ursachen:
1. **`markRead` nahm fremde Beitrags-Kennungen ungeprüft an.** Ein an Trakk gebundener Client konnte einen MTA360-Beitrag als gelesen melden, die Zeile landete mit falschem Projekt in der Tabelle und senkte den Zähler der fremden App. Die Zeilen werden jetzt aus der Datenbank abgeleitet, eingeschränkt auf Projekt, Status `published` und die für den Client sichtbaren Zielgruppen.
2. **Unbekannte Beitrags-Kennung führte zu 500.** Der Fremdschlüssel schlug durch. Das war zusätzlich ein Auskunftskanal, weil existierende Kennungen 200 und unbekannte 500 ergaben. Jetzt antwortet der Endpunkt 200 mit `marked: 0` und verrät nichts.
3. **Seitenaufteilung ohne eindeutige Sortierung.** Bei gleichem `publishAt` war die Reihenfolge zufällig, Beiträge konnten auf zwei Seiten doppelt oder gar nicht erscheinen. Sortiert wird jetzt nach `publish_at desc nulls last`, dann nach `id`.
4. **Stillgelegte Projekte lieferten weiter Beiträge aus,** obwohl `GET /api/v1/projects` sie ausblendet. `listPosts`, `findPost` und `listUnread` prüfen jetzt `project.is_active`.
Dazu Testlücken geschlossen: Projektbindung des Clients, vollständige Übergangsmatrix mit allen 25 Paaren, Zielgruppe bis zur HTTP-Antwort inklusive `scope public`, Form des Problem-JSON, Parametergrenzen, `since` als Gleichheitsgrenze, Schlagwort-Filter mit zwei Schlagworten ohne Doppelzählung, Seed-Idempotenz ohne Dubletten, 422 für Clients ohne Projektbindung.
## Bewusst nicht behoben
| Fund | Warum nicht |
|---|---|
| Beitrags-Detail liefert keine Blöcke und Medien | Plan 1 liefert genau die Listenfelder. Blöcke kommen mit dem Web-Archiv in Plan 3 |
| Schema erlaubt einen Write-Client mit `can_publish` | Es gibt noch keine Schreib-API. Die Sperre gehört in Plan 5, dort mit Test |
| Lese-Endpunkte prüfen den Client-Modus nicht | Ein Write-Client darf lesen. Die Zielgruppe schützt weiterhin, der Modus ist keine zweite Schranke |
| Kein Index deckt die Hauptabfrage der Liste | Bei dieser Datenmenge sinnlos. Gehört gemessen, nicht geraten |
| `listUnread` lädt alle ungelesenen Zeilen zum Zählen | Zählt Beiträge eines Projekts, das bleibt klein. Bei Bedarf später eine Zählabfrage |
| Seed-Token steht im Klartext in `scripts/seed.ts` | Reines Entwicklungs-Token für die lokale Datenbank, `lb_seed_trakk_read`. Darf niemals in einer erreichbaren Umgebung gelten |
| Zusammengesetzter Fremdschlüssel auf `post_read` | Die Anwendung verhindert falsche Zuordnungen jetzt zuverlässig, geprüft per Mutationsprobe. Ein zusätzlicher Datenbank-Zwang wäre Tiefenverteidigung und kostet eine Migration plus zusätzlichen Index |
## Lokale Abnahme
```bash
cd /Volumes/M2mini/WORK/NYO/projects/logbuch
docker compose up -d
pnpm dev
```
Dann:
```bash
curl -s -H "Authorization: Bearer lb_seed_trakk_read" "http://localhost:4700/api/v1/posts"
curl -s -H "Authorization: Bearer lb_seed_trakk_read" "http://localhost:4700/api/v1/unread?external_user_id=u-1"
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:4700/api/v1/posts"
```
Erwartet: zwei Trakk-Beiträge, kein interner Beitrag, ohne Token 401.
Das Design des Web-Archivs liegt als Entwurf unter `docs/design/mockup-overview.html` und ist im Browser direkt öffenbar.
## Nächster Schritt
Plan 2: Auth, Admin, Block-Editor, Freigabe. Offen davor: Subdomain, und ob der Admin-Login mit better-auth per Mail und Passwort startet oder auf Portal-SSO wartet.