# Repomap — Munin Übersicht aller Dateien und Ordner mit ihrer Aufgabe. ``` munin/ ├── main.go ← Einstiegspunkt. Liest die Konfiguration, prüft die │ Pflichtangaben (SESSION_SECRET, ADMIN_TOKEN inkl. │ Mindestlänge), öffnet Kontroll-Datenbank und │ Workspace-Verwaltung (legt beim ersten Start den │ Workspace "Intern" an), setzt die │ Bestandteile zusammen und startet den Server. │ Enthält außerdem die Server-Timeouts (gegen │ Slowloris), die Body-Größenlimits und die │ täglichen Aufgaben (Protokoll aufräumen, │ Export), die über fuerJedenWorkspace alle │ Workspaces erreichen. Tests: main_test.go. │ ├── go.mod / go.sum ← Moduldefinition und Prüfsummen der Abhängigkeiten. │ ├── Dockerfile ← Zweistufiger Bau: Stufe 1 kompiliert mit dem │ Go-Compiler, Stufe 2 packt nur das fertige Binary │ in ein minimales Alpine-Abbild. Läuft als │ gewöhnlicher Benutzer, mit Healthcheck und Volume │ für die Datenbank. │ ├── .env.example ← Vorlage für .env, mit Erklärung jeder Variable. ├── .gitignore / .dockerignore ← Schließen Datenbank, Binary und Geheimnisse aus. │ ├── README.md ← Anleitung: Schnellstart, MCP einrichten, Werkzeuge, │ Konfiguration, Sicherheit, Betrieb. ├── AGENTEN-GUIDE.md ← Wie man KI-Werkzeuge anbindet und anspricht, │ damit sie den Speicher tatsächlich nutzen: │ Projektprompt zum Übernehmen, slug-Regeln, │ Muster für einzelne Aufgaben. ├── AUSBAUSTUFEN.md ← Was bewusst nicht gebaut wurde: Messwerte, │ Begründung und die Auslöser, an denen man │ merkt, dass es Zeit dafür wird. ├── REPOMAP.md ← Diese Datei. │ ├── internal/ │ │ │ ├── store/ ── Die einzige Stelle, die SQL kennt ────────────── │ │ ├── store.go ← Öffnet eine Workspace-Datei (Open), setzt die │ │ │ nötigen PRAGMAs (WAL, busy_timeout, foreign_keys) │ │ │ und wendet die Migrationen an. Erklärt auch, warum │ │ │ der Treiber modernc.org/sqlite ist: Er ist reines │ │ │ Go und erlaubt damit ein statisches Binary. │ │ │ │ │ ├── control.go ← Die zentrale Kontroll-Datenbank (OpenControl): │ │ │ Workspaces anlegen, umbenennen, löschen, dazu die │ │ │ Zuordnung Key-Prüfsumme → Workspace │ │ │ (ResolveKeyWorkspace). Das ist der erste Schritt │ │ │ jeder Anmeldung: Erst danach ist klar, welche │ │ │ Workspace-Datei den Key kennt. │ │ │ │ │ ├── models.go ← Die Datentypen (Project, APIKey, Entry, …), die │ │ │ Fehlerwerte (ErrNotFound, ErrConflict, │ │ │ ErrDuplicate) sowie ID- und Zeitbehandlung. │ │ │ NewID erzeugt sortierbare IDs in Go statt per │ │ │ AUTOINCREMENT — das hält den Umstieg auf eine │ │ │ andere Datenbank offen. │ │ │ │ │ ├── projects.go ← Projekte: anlegen, lesen, ändern, löschen. │ │ │ Enthält ValidSlug und Slugify sowie │ │ │ SetProjectWriteLimit für die eigene Obergrenze │ │ │ eines Projekts. │ │ │ │ │ ├── keys.go ← API-Keys. ResolveKeyHash ist der heißeste Pfad im │ │ │ Programm (jeder MCP-Request) und deshalb ein │ │ │ indizierter Lookup über die Prüfsumme, keine │ │ │ Schleife. Zurückgezogene Keys gelten als nicht │ │ │ vorhanden, der Datensatz bleibt fürs Protokoll. │ │ │ │ │ ├── entries.go ← Einträge. PutEntry legt an ODER aktualisiert — │ │ │ der Bezeichner entscheidet, und genau das ist der │ │ │ Mechanismus gegen Dubletten. Enthält das │ │ │ optimistische Sperren (ExpectedVersion) und das │ │ │ Nachladen der Schlagworte in einer Abfrage statt │ │ │ einer pro Eintrag. Außerdem: │ │ │ · snapshotEntryVersion sichert vor jeder Änderung │ │ │ die bisherige Fassung, in derselben Transaktion; │ │ │ EntryVersions/EntryVersionByNumber lesen sie │ │ │ · BulkUpdateTags ändert Schlagworte vieler │ │ │ Einträge auf einmal und überspringt IDs aus │ │ │ fremden Projekten │ │ │ · LinkGraph liefert Knoten und Kanten für die │ │ │ Graph-Ansicht │ │ │ Jede lesende Abfrage trägt nichtImPapierkorb │ │ │ (deleted_at IS NULL) — außer PutEntry, das │ │ │ einen Eintrag im Papierkorb bewusst sieht und │ │ │ ihn beim Überschreiben zurückholt. │ │ │ │ │ ├── nutzung.go ← Kennzahlen der Nutzungsübersicht: UsageSummaries │ │ │ (Schreibvorgänge der letzten 30 Tage aus dem │ │ │ Protokoll, Bestand aus entries — für ein oder │ │ │ alle Projekte in zwei Abfragen), dazu │ │ │ MissingLinks, UntaggedEntries, RecentlyUsedKeys. │ │ │ Keine eigene Zähltabelle. │ │ │ │ │ ├── papierkorb.go ← Papierkorb: MoveEntryToTrash (statt DELETE), │ │ │ ListTrash, RestoreFromTrash, PurgeFromTrash │ │ │ (nur was schon im Papierkorb liegt) und │ │ │ PurgeTrash für die tägliche Aufgabe. Die Frist │ │ │ TrashRetention (30 Tage) steht hier, damit │ │ │ Oberfläche und Aufräumen dieselbe Zahl nutzen. │ │ │ │ │ ├── links.go ← Erkennt [[bezeichner]] im Inhalt und ersetzt sie │ │ │ beim Rendern. Das ersetzt die frühere │ │ │ Ordnerhierarchie als Struktur zwischen Einträgen. │ │ │ │ │ ├── audit.go ← Protokoll: schreiben, lesen, aufräumen. Die │ │ │ Aufbewahrungsfrist ist nicht nur eine Frage des │ │ │ Plattenplatzes — das Protokoll enthält Adressen │ │ │ und Zugangsnamen. EntryWriteDetail vermerkt bei │ │ │ Schreibvorgängen "angelegt" oder "aktualisiert". │ │ │ │ │ ├── migrations_control/ │ │ │ └── 001_init.sql ← Schema der Kontroll-Datenbank: workspaces und │ │ │ key_index (Key-Prüfsumme → Workspace). Bewusst │ │ │ dünn — Inhalte liegen nie hier. │ │ │ │ │ └── migrations/ ← Schema jeder Workspace-Datei │ │ ├── 001_init.sql ← Das Schema. Der Kopfkommentar nennt die Regeln │ │ │ für portables SQL, an die sich alle weiteren │ │ │ Migrationen halten sollen. │ │ ├── 002_projekt_in_nebentabellen.sql │ │ ← Führt project_id auch in entry_tags und │ │ entry_links. Eine bewusste Redundanz gegen zwei │ │ gemessene Bremsen: das Schlagwortmenü lief über │ │ alle Einträge (6,7 ms bei jedem Seitenaufruf), │ │ und bei den Rückverweisen kippte durch ein │ │ ORDER BY die Verknüpfungsreihenfolge │ │ (3,1 ms statt 9 µs). │ │ ├── 003_schreibbudget_je_projekt.sql │ │ ← Spalte write_limit_hourly an projects. Vorgabe 0 │ │ heißt "keine eigene Angabe" — dann gilt der │ │ Standard aus WRITE_LIMIT_HOURLY. So muss die │ │ Migration keine Daten anfassen. │ │ ├── 004_aenderungshistorie.sql │ │ ← Tabelle entry_versions für abgelöste Fassungen. │ │ Wird mit dem Eintrag gelöscht (ON DELETE CASCADE). │ │ └── 005_papierkorb.sql │ │ ← deleted_at/deleted_by an entries und ein │ │ Teilindex nur über den Papierkorb. Löschen setzt │ │ nur noch die Markierung; CASCADE greift erst │ │ beim endgültigen Löschen. │ │ │ ├── workspace/ ── Welche Datei gehört zu welcher Abteilung? ────── │ │ └── workspace.go ← Manager öffnet Workspace-Dateien bei Bedarf und │ │ hält sie offen, jeweils mit eigenem Suchindex. │ │ Datenbank und Index gehören zusammen, damit kein │ │ Index versehentlich an der falschen Datei hängt. │ │ Delete schließt eine Datei und entfernt sie. │ │ │ ├── auth/ ── Wer ist das? ──────────────────────────────────── │ │ ├── doc.go ← Erklärt die beiden Wege hinein (API-Key im Header, │ │ │ Sitzungs-Cookie im Browser) und was hier bewusst │ │ │ NICHT liegt: die Frage, was jemand darf. │ │ │ │ │ ├── keys.go ← Erzeugen, Prüfsumme bilden, Form prüfen. Enthält │ │ │ die Rollen (read/write/admin) und die Begründung, │ │ │ warum SHA-256 statt bcrypt richtig ist. │ │ │ │ │ ├── session.go ← Signierte Sitzungs-Cookies (HMAC-SHA256). Der │ │ │ Cookie trägt nur eine Behauptung ("ich bin Key X"), │ │ │ nie den Key selbst. │ │ │ │ │ ├── csrf.go ← CSRF-Tokens aus Sitzung und Geheimnis, ohne Zustand │ │ │ auf dem Server. Der Kopfkommentar erklärt den │ │ │ Angriff, gegen den das schützt. │ │ │ │ │ ├── cookies.go ← Setzt HttpOnly, SameSite und Secure an einer │ │ │ Stelle. Secure ist ohne Konfiguration an, außer │ │ │ bei localhost — damit die lokale Entwicklung über │ │ │ http weiter funktioniert. │ │ │ │ │ ├── ratelimit.go ← Zwei Begrenzungen auf einem gemeinsamen │ │ │ Fixed-Window-Zähler: RateLimiter sperrt eine │ │ │ Adresse nach zu vielen Fehlversuchen, │ │ │ WriteBudget deckelt die schreibenden │ │ │ MCP-Aufrufe je Projekt und Stunde. Letzteres │ │ │ hängt am Projekt und nicht am Key — ein │ │ │ Key-Limit ließe sich mit einem zweiten Key │ │ │ umgehen. │ │ │ │ │ └── trustedproxy.go ← Entscheidet, wann X-Forwarded-For geglaubt wird: │ │ nur bei einer über TRUSTED_PROXY hinterlegten │ │ Absenderadresse. Ohne Konfiguration wird der │ │ Header ignoriert — er ist frei fälschbar. │ │ │ ├── entry/ ── Wie sieht ein Eintrag aus? ───────────────────── │ │ ├── entry.go ← Markdown nach HTML (über internal/markdown), │ │ │ Auflösen der [[…]]-Verweise, Textausschnitte für │ │ │ Trefferlisten samt sicherer Hervorhebung. │ │ │ │ │ └── diff.go ← Zeilenvergleich zweier Fassungen (DiffLines) als │ │ LCS ohne externe Abhängigkeit. Bei sehr langen │ │ Einträgen weicht er auf einen groben Vergleich │ │ aus (grobDiff), damit Speicher und Rechenzeit │ │ begrenzt bleiben. │ │ │ ├── markdown/ ── Markdown nach HTML, eine Regel für alles ─────── │ │ └── markdown.go ← Der gemeinsame goldmark-Renderer für Einträge und │ │ Hilfe: GFM (Tabellen, Checklisten, │ │ Durchstreichen, automatische Links), Roh-HTML │ │ gesperrt. Nur die Hilfe bekommt Überschrift- │ │ Anker (RenderMitAnkern) — mit gefalteten │ │ Umlauten über store.Slugify, damit die │ │ "?"-Links der Oberfläche darauf zeigen können. │ │ │ ├── export/ ── Sicherung als Markdown ───────────────────────── │ │ └── export.go ← Schreibt jeden Workspace als Ordner │ │ (/, mit _workspace.md) und darin │ │ jedes Projekt als Ordner voller │ │ Markdown-Dateien, mit allen Feldern im Dateikopf. │ │ Kennt selbst keine Datenbank: Der Aufrufer gibt │ │ die Workspace-Datei je Lauf mit. │ │ Gedacht als Git-fähige Sicherung: Ein Lauf ohne │ │ inhaltliche Änderungen fasst keine Datei an und │ │ erzeugt deshalb auch keinen Diff. Verwaiste │ │ Dateien gelöschter Einträge werden entfernt, │ │ fremde Dateien im Zielordner bleiben unberührt. │ │ │ ├── search/ ── Volltextsuche ────────────────────────────────── │ │ ├── index.go ← Invertierter Index mit BM25-Bewertung. Titel zählen │ │ │ dreifach, Schlagworte zweifach. Enthält die │ │ │ Präfix-Rückfallebene: Ohne sie findet "deploy" │ │ │ den "Deployment-Prozess" nicht — bei einer │ │ │ KI-Anfrage ein stiller Fehltreffer. │ │ │ │ │ └── registry.go ← Ein Index je Projekt, verzögert aufgebaut. Getrennte │ │ Indizes statt eines gemeinsamen mit Filter: So wird │ │ die Abschottung strukturell, und die BM25-Werte │ │ werden nicht über Projektgrenzen hinweg verzerrt. │ │ │ ├── hilfe/ ── Eingebaute Onboarding-Hilfe ─────────────────── │ │ ├── hilfe.go ← Liste der Hilfeseiten (Reihenfolge, Titel) und │ │ │ das Rendern mit Überschrift-Ankern. Erklärt, │ │ │ warum die Hilfe im Binary steckt statt als │ │ │ Einträge in der Datenbank: Sie muss zur │ │ │ laufenden Fassung des Programms passen. │ │ └── seiten/*.md ← Die Inhalte, je Seite eine Datei. {{SERVER}} und │ │ {{PROJEKT}} werden beim Anzeigen ersetzt. │ │ │ ├── web/ ── HTTP: Browser und API ────────────────────────── │ │ ├── context.go ← Session-Typ (wer greift zu, was darf er) und die │ │ │ Ablage im Request-Context. CanAccessProject ist │ │ │ die Mandantentrennung der Weboberfläche. │ │ │ │ │ ├── middleware.go ← Sitzungsprüfung (inkl. "gilt der Key noch?"), │ │ │ CSRF, Projektauflösung aus der URL, Rollenprüfung. │ │ │ Die Sitzung trägt die Workspace-ID signiert mit │ │ │ (sessionFromSubject); requireWorkspace hält einen │ │ │ Plattform-Admin ohne gewählten Workspace von den │ │ │ Projektseiten fern. │ │ │ Dazu die allgemeinen Schutzschichten │ │ │ SecurityHeaders und LimitBody sowie APIAuth für │ │ │ den MCP-Endpunkt. │ │ │ │ │ ├── server.go ← Der Server-Typ, alle Routen und das Rendern der │ │ │ Templates (erst in einen Puffer, dann senden — │ │ │ sonst gäbe es bei einem Fehler eine halbe Seite │ │ │ mit Status 200). │ │ │ │ │ ├── handlers_auth.go ← Anmelden, Abmelden, Startseite. Der │ │ │ Plattform-Admin ohne gewählten Workspace landet │ │ │ auf /admin/workspaces. │ │ ├── handlers_entries.go ← Einträge auflisten, suchen, ansehen, bearbeiten. │ │ │ Dazu Vorschau (preview), Vorschläge für [[…]] │ │ │ (suggest), Fassungen und Vergleich, Graph-Daten │ │ │ und Schlagworte für viele Einträge (bulk-tags). │ │ ├── handlers_nutzung.go ← Nutzungsseite je Projekt (/p/{projekt}/usage), │ │ │ für jede Rolle. Die Admin-Projektliste nutzt │ │ │ dieselben Zahlen (projectListItems). │ │ ├── handlers_papierkorb.go │ │ │ ← Papierkorb je Projekt: ansehen, wiederherstellen, │ │ │ endgültig löschen, Filter nach löschendem Zugang. │ │ │ Alles nur mit Schreibrecht, und bewusst nur hier │ │ │ — über MCP gibt es kein Wiederherstellen. │ │ ├── handlers_hilfe.go ← Zeigt die eingebauten Hilfeseiten unter /hilfe │ │ │ für jede Sitzung. Setzt vorher Server-Adresse │ │ │ und Projekt-Bezeichner in die Beispiele ein │ │ │ (serverURL, mit geprüftem Host-Header). │ │ ├── handlers_workspaces.go │ │ │ ← Workspaces auflisten, anlegen, wählen, umbenennen, │ │ │ löschen (nur Plattform-Admin). Löscht jemand den │ │ │ Workspace der eigenen Sitzung, wird die Auswahl │ │ │ zurückgesetzt, statt ihn auszusperren. │ │ └── handlers_admin.go ← Projekte, Zugänge, Protokollansicht, Export. Die │ │ Bearbeitungsseite je Projekt (Name, │ │ Beschreibung, Schreibbudget, Löschen) liegt │ │ hier — bewusst eine Seite je Projekt statt │ │ eines Auswahlfelds über alle: Nur so stehen die │ │ tatsächlichen Werte in den Feldern, und ein │ │ Speichern ohne Änderung überschreibt nichts. │ │ │ └── mcpserver/ ── Die Hauptschnittstelle ──────────────────────── │ ├── server.go ← Handler und Transport (Streamable HTTP), ein │ │ MCP-Server je Projekt. Enthält den Text der │ │ `instructions` — das wirksamste Stellrad für die │ │ Qualität der Antworten, weil er bei jedem Client │ │ im Kontext steht. │ │ │ └── tools.go ← Die fünf Werkzeuge: recall, get, list, remember, │ forget. Jedes ist eine Closure über genau ein │ Projekt und kann strukturell nicht an ein anderes. │ ├── templates/ ← HTML der Weboberfläche. partials.html enthält die │ gemeinsamen Blöcke "kopf" und "fuss", die jede │ Seite einbindet — kein verschachteltes Layout. │ ├── static/ │ ├── style.css ← Das gesamte Aussehen, ohne Framework. Farben als │ │ Variablen ganz oben, helle und dunkle Darstellung. │ ├── confirm.js ← Bestätigungsabfrage vor dem Löschen. Liest das │ │ Attribut data-confirm, statt onclick="" im HTML zu │ │ verwenden — Inline-JavaScript verbietet die │ │ Content-Security-Policy. │ ├── preview.js ← Reiter "Schreiben"/"Vorschau" im Editor. Holt das │ │ HTML vom Server, statt Markdown im Browser zu │ │ rendern. Ohne JavaScript bleibt das Formular │ │ unverändert nutzbar. │ ├── autocomplete.js ← Vorschläge nach "[[" im Textfeld, per Tastatur │ │ und Maus bedienbar. │ ├── graph.js ← Selbst geschriebene Kräftesimulation und │ │ SVG-Darstellung für die Graph-Ansicht. Texte nur │ │ über textContent, nie innerHTML. │ └── img/ ← Logo in mehreren Fassungen (hell, dunkel, ohne │ Hintergrund). Die Anmeldeseite wählt je nach │ Farbschema die passende. │ ├── scripts/ │ └── backup.sh ← Sichert control.db und jede Workspace-Datei │ einzeln über sqlite3 .backup in einen │ zeitgestempelten Ordner; RETENTION_DAYS räumt │ alte Sicherungen auf. │ └── data/ ← Daten (nicht im Repository, als Volume gemountet) ├── control.db ← Kontroll-Datenbank: Workspaces, Key-Zuordnung └── workspaces/ └── .db ← Eine Datei je Workspace ``` ## Tests Jedes Package hat seine Tests direkt daneben. Sie laufen mit `go test ./...` ohne weitere Vorbereitung — die Datenbanktests legen jeweils eine echte SQLite-Datei in einem Testverzeichnis an, damit auch Migrationen und Fremdschlüssel geprüft werden. | Datei | Schwerpunkt | |---|---| | `internal/auth/auth_test.go` | Signatur, Manipulation, Ablauf, CSRF, Key-Form, Rollen, Cookie-Flags, Rate-Limiting, X-Forwarded-For | | `internal/store/nutzung_test.go` | Zählung nach Art und Weg samt Zeitfenster, Bestand ohne Papierkorb, Verweise ins Leere, aktive Zugänge | | `internal/store/papierkorb_test.go` | **Jeder lesende Weg blendet den Papierkorb aus** (Einzelabruf, Liste mit Filtern, Suchindex/Export, Zählen, Schlagworte, Rückverweise, Graph, Sammelbearbeitung); Wiederherstellen, Zurückholen durch Überschreiben, Konflikt, endgültiges Löschen, Frist | | `internal/store/store_test.go` | Migrationen, Slug-Prüfung, Abschottung, Dublettenvermeidung, optimistisches Sperren, Verweise, Protokoll, Änderungshistorie | | `internal/store/control_test.go` | Kontroll-Datenbank: Standard-Workspace bleibt stabil, Auflistung, Zuordnung Key → Workspace | | `internal/workspace/workspace_test.go` | Manager hält Dateien offen, trennt Workspaces, schließt sauber | | `internal/entry/entry_test.go` | HTML-Escaping, Verweisauflösung, Textausschnitte, sichere Hervorhebung | | `internal/markdown/markdown_test.go` | GFM-Erweiterungen, kein Roh-HTML und keine `javascript:`-Links, Anker nur in der Hilfe | | `internal/entry/diff_test.go` | Zeilenvergleich, Grenzfälle, Ausweichen bei sehr langen Einträgen | | `internal/search/search_test.go` | Bewertung, Präfix-Rückfallebene, Aktualisieren und Entfernen | | `internal/export/export_test.go` | Dateikopf und YAML-Maskierung, Aufräumen verwaister Dateien, Stabilität über mehrere Läufe | | `internal/web/web_test.go` | Anmeldung, zurückgezogene Keys, CSRF, **Projektabschottung**, **Workspace-Trennung**, Rollen, Header, Hilfeseiten samt eingesetzter Adresse | | `internal/mcpserver/mcpserver_test.go` | Werkzeugbestand, `instructions`, Dublettenvermeidung, Konflikte, Rollen, **Projektabschottung** | | `internal/hilfe/hilfe_test.go` | Seitenliste und Dateien passen zusammen, interne Links führen zu vorhandenen Seiten, kein Roh-HTML | ## Datenfluss: Agent ruft Wissen ab ``` KI-Werkzeug POST /mcp { "method": "tools/call", "params": { "name": "recall", … } } → web/middleware.go (SecurityHeaders, LimitBody) → web/middleware.go (APIAuth) Key → Prüfsumme → store/control.go (ResolveKeyWorkspace) → Workspace → workspace/workspace.go (Get) → dessen Datei und Suchindex → store/keys.go (ResolveKeyHash) → Projekt + Rolle in den Context; Fehlversuche in die Sperre → mcpserver/server.go (ServeHTTP) Projekt aus dem Context → dessen MCP-Server → mcpserver/tools.go (recall) → search/registry.go Index dieses Projekts (beim ersten Mal aus der Datenbank aufgebaut) → search/index.go (Search) BM25, bei Bedarf Präfix-Rückfallebene → store/entries.go (EntryBySlug) Treffer nachladen → entry/entry.go (Snippet) Textausschnitt je Treffer → JSON-Antwort mit Trefferliste und Hinweis zum weiteren Vorgehen ``` ## Datenfluss: Agent legt Wissen ab ``` KI-Werkzeug POST /mcp { … "name": "remember", "arguments": { "slug": …, "content": … } } → web/middleware.go (APIAuth) wie oben → mcpserver/tools.go (remember) → requireWrite() Rolle read wird hier abgewiesen → store/entries.go (PutEntry) eine Transaktion: · Bezeichner vorhanden? → aktualisieren · ExpectedVersion prüfen → ggf. Konflikt · Schlagworte und Verweise ersetzen · Herkunft (agent) und Zugangsname setzen → search/registry.go (Update) Index nachziehen → store/audit.go (LogAudit) wer, was, wann → Antwort: Bezeichner, neue Fassung, angelegt oder aktualisiert ``` ## Datenfluss: Mensch bearbeitet im Browser ``` Browser POST /p/plattform/e/deployment/save → web/middleware.go (SecurityHeaders, LimitBody) → web/middleware.go (requireSession) Cookie prüfen → Key aus der Datenbank laden → gilt er noch? → CSRF-Token prüfen → web/middleware.go (requireProject) Projekt aus der URL; fremdes Projekt → 404 → web/middleware.go (requireWrite) Rollenprüfung → web/handlers_entries.go (handleEntrySave) → store/entries.go (PutEntry) Herkunft: human, Autor: Key-Name → search/registry.go (Update) → store/audit.go (LogAudit) → Weiterleitung zur Detailansicht ```