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
+188
View File
@@ -0,0 +1,188 @@
# Logbuch API
Stand: 31.07.2026. Diese Datei beschreibt den tatsächlichen Stand. Was noch nicht gebaut ist, steht am Ende unter "Noch nicht vorhanden" und ist dort auch so gekennzeichnet.
## Wofür Logbuch da ist
Logbuch hält fest, was in den Produkten der Firmengruppe passiert ist. Alle zwei Wochen entsteht pro Projekt ein Eintrag: eine neue Funktion, eine Verbesserung, ein behobener Fehler, eine Änderung mit Auswirkung.
Es ist kein Änderungsprotokoll für Entwickler. Es ist für Kollegen und später Kunden, die wissen wollen, was sich geändert hat, ohne Tickets oder Code zu lesen.
Die Anwendung besteht aus drei Teilen: dem Archiv zum Lesen, dem Adminbereich zum Schreiben, und dieser API. Über die API holen andere Anwendungen ihre Einträge und zeigen sie ihren Nutzern an, und über sie können Einträge verfasst werden.
## Grundlagen
Basis-Adresse lokal: `http://localhost:4700`
Alle Endpunkte liegen unter `/api/v1`. Angemeldet wird sich mit einem Token im Kopf:
```
Authorization: Bearer <token>
```
Zugänge werden im Adminbereich unter Verwaltung, Zugänge angelegt. Beim Anlegen wird das Token einmal im Klartext gezeigt, danach nie wieder. Gespeichert ist nur ein Hash.
Ein Zugang hat drei Eigenschaften, die zusammen bestimmen, was er darf:
| Eigenschaft | Bedeutung |
|---|---|
| Modus | `read` liest, `write` schreibt zusätzlich |
| Zielgruppe | `internal`, `customer` oder `public`. Bestimmt, welche Einträge der Zugang überhaupt sieht |
| Projektbindung | Ist ein Projekt gesetzt, sieht und schreibt der Zugang ausschließlich dort |
Die Zielgruppe ist die wichtigste Einstellung. Ein Zugang mit `customer` sieht Kundenbeiträge und öffentliche, aber niemals interne. Das gilt serverseitig, kein Parameter kann das umgehen.
Ein Zugang mit Schreibrecht kann **niemals veröffentlichen**. Alles landet als Entwurf, ein Mensch gibt frei.
Fehler kommen einheitlich als Problem-JSON:
```json
{ "type": "unauthorized", "title": "unauthorized", "status": 401, "detail": null }
```
## Lesen
### GET /api/v1/projects
Die Projekte, die der Zugang sehen darf.
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:4700/api/v1/projects
```
Antwort: `{ "items": [ { "id", "slug", "name", "code", "color", "description", "sort", "isActive" } ] }`
### GET /api/v1/posts
Die veröffentlichten Einträge, neueste zuerst.
| Parameter | Wirkung |
|---|---|
| `project` | Slug eines Projekts |
| `since` | Zeitpunkt, ab dem gesucht wird |
| `type` | `feature`, `improvement`, `fix`, `breaking`, `info` |
| `tag` | Schlagwort |
| `page` | Seite, ab 1 |
| `per_page` | 1 bis 100, Standard 25 |
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:4700/api/v1/posts?project=trakk&per_page=5"
```
Antwort: `{ "items": [...], "meta": { "total", "page", "per_page" } }`
Ein Eintrag in der Liste trägt: `id`, `slug`, `title`, `teaser`, `type`, `audience`, `publishAt`, `number`, `projectSlug`, `projectName`, `projectCode`, `projectColor`.
### GET /api/v1/posts/:slug
Ein einzelner Eintrag. Ist er für die Zielgruppe des Zugangs nicht sichtbar, kommt 404, nicht 403. Damit verrät die Antwort nicht, dass es ihn gibt.
### GET /api/v1/unread
Was ein Nutzer der einbindenden Anwendung noch nicht gelesen hat.
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:4700/api/v1/unread?external_user_id=u-42"
```
`external_user_id` ist die Nutzerkennung der aufrufenden Anwendung, nicht die eines Logbuch-Kontos. Antwort: `{ "count", "items": [...] }`.
Der Zugang muss dafür an ein Projekt gebunden sein, sonst 422.
### POST /api/v1/read
Markiert Einträge als gelesen.
```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"external_user_id":"u-42","post_ids":["<id>"]}' \
http://localhost:4700/api/v1/read
```
Antwort: `{ "marked": 1 }`, also die Zahl der tatsächlich neu vermerkten Einträge. Unbekannte Kennungen und Einträge fremder Projekte werden still verworfen, die Antwort bleibt 200.
## Bilder
### POST /api/v1/media
Lädt ein Bild hoch, erzeugt WebP-Varianten und legt einen Datensatz an. Braucht einen Zugang mit Schreibrecht.
```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@screenshot.png" \
-F "projectId=<projekt-id>" \
-F "alt=Spaltenmenü mit den neuen Einträgen" \
http://localhost:4700/api/v1/media
```
Grenzen: nur Bildformate, höchstens 15 MB.
### GET /api/v1/media/file/...
Liefert eine Variante aus. Die Pfade stehen am Medium unter `variants`, je Eintrag mit `width`, `format` und `path`.
## Redaktionsleitfaden
Wer über die API schreibt, schreibt für Menschen. Diese Regeln gelten für jeden Eintrag.
**Aufbau.** Titel, Anreißer, dann der Text. Der Titel sagt, was sich geändert hat, nicht dass sich etwas geändert hat. Der Anreißer fasst in zwei bis drei Sätzen zusammen, was der Leser davon hat. Der Text erklärt, was vorher war, was jetzt ist, und was zu tun bleibt.
**Gute Titel:**
- "SLA-Uhr zählt Feiertage nicht mehr mit"
- "Spalten per Rechtsklick verwalten"
**Schlechte Titel:**
- "Verbesserungen an der Tabelle" (sagt nichts)
- "Wir haben ein spannendes neues Feature gebaut" (Werbung, keine Information)
**Ton.** Sachlich und direkt. Aktive Verben. Keine Werbesprache, keine Superlative, keine Ausrufezeichen, keine Emojis. Keine Gedankenstriche, immer normales Minus. Echte Umlaute.
**Länge.** So lang wie nötig. Meist drei bis sechs Absätze. Ein Fehlerbehebung darf auch aus zwei Sätzen bestehen.
**Bilder.** Zeigen, was sich geändert hat, nicht das Firmenlogo. Jedes Bild bekommt einen Alt-Text, der beschreibt was zu sehen ist. Bei öffentlichen Beiträgen ist der Alt-Text Pflicht, sonst lässt sich der Beitrag nicht veröffentlichen.
**Zielgruppe.** `internal` für alles, was intern bleibt: Umbauten, Zwischenstände, Interna über Kunden. `customer` für alles, was ein Kunde des Produkts wissen soll. `public` nur für das, was auch außerhalb stehen darf.
**Wer veröffentlicht.** Nicht der Zugang. Ein Mensch schaut drauf und gibt frei. Ein über die API verfasster Beitrag ist immer ein Vorschlag.
## Noch nicht vorhanden
Ehrlich benannt, damit niemand daran vorbei entwickelt. Diese Endpunkte werden gerade gebaut:
| Geplant | Zweck |
|---|---|
| `POST /api/v1/posts` | Beitrag als Entwurf anlegen |
| `PATCH /api/v1/posts/:id` | Entwurf ändern |
| `PUT /api/v1/posts/:id/blocks` | Inhaltsblöcke setzen |
| `GET /api/v1/posts/:id/preview` | Entwurf zurücklesen |
| `DELETE /api/v1/posts/:id` | Entwurf löschen |
| `GET /api/v1/post-types` | verwaltbare Beitragsarten |
| `GET /api/v1/style-guide` | dieser Leitfaden, maschinenlesbar |
| `GET /api/v1/openapi.json` | maschinenlesbare Beschreibung |
| `/admin/api` | diese Dokumentation im Adminbereich |
Solange sie fehlen, kann ein Zugang mit Schreibrecht ausschließlich Bilder hochladen.
## Blocktypen
Der Inhalt eines Beitrags besteht aus Blöcken in fester Reihenfolge.
| Typ | Daten |
|---|---|
| `text` | `text` |
| `image` | `mediaId` |
| `gallery` | `mediaIds` |
| `before_after` | `beforeMediaId`, `afterMediaId` |
| `video` | `url`, `title` |
| `quote` | `text`, `source` |
| `code` | `code`, `language` |
| `link` | `url`, `title`, `description` |
| `callout` | `text`, `tone` |
Unbekannte Typen werden bei der Anzeige übersprungen, sie zerlegen die Seite nicht.