# archivdms – Dev Log ## 2026-08-11 – FDN-08 Nachbesserung: Login-Request erzeugte keine Logzeile mit request_id **Zeit:** ca. 0,5 h (Verifikationsbefund nachvollzogen, Ursache eingegrenzt, Access-Log + Auth-Logging ergänzt, Symbole gegengeprüft) **Befund vom Deploy auf 192.168.1.204:** Test-Request gegen `/api/auth/login` erzeugte keine Zeile mit `request_id`. **Es war KEINE Regression der Call-Site-Umstellung** — der Grep über `internal/api/*.go` bestätigt: außer den zwei dokumentierten Aufrufen in `SetStorageConfig` (`server.go:113/117`, kein Request-Kontext) existiert kein `s.logger.` mehr. Zwei Vorlücken waren die Ursache: 1. `handleLogin`/`handleLogout`/`handleMe` in `internal/api/auth_handlers.go` haben **noch nie** technisch geloggt — nur Audit-Log-Einträge geschrieben. `s.reqLog` konnte dort nichts umstellen, weil es keine Call-Site gab. 2. `metricsMiddleware` schrieb nur bei `status >= 500` eine Zeile. Ein erfolgreicher (200) oder abgelehnter (401) Login lief damit komplett lautlos durch — AK1 war faktisch nur für Fehler-Requests belegbar. **Geändert:** - `internal/api/observability.go`: `metricsMiddleware` schreibt jetzt für **jede** Anfrage genau eine Access-Log-Zeile mit `request_id`, gestaffelt nach Status (5xx=Error „request failed", 4xx=Warn „request rejected", Rest=Debug „request completed") inkl. `bytes`. Debug für den Normalfall, damit das Log bei Level Info nicht zuläuft; unter `LOG_LEVEL=debug` ist jeder Request nachverfolgbar. - `internal/api/auth_handlers.go`: `handleLogin` loggt Body-Parse-Fehler (Warn), Fehlschlag (Warn, `username`+`remote_ip`+`reason`, **kein** Passwort/keine Auth-Interna) und Erfolg (Info, `user_id`/`username`/`tenant_id`/`remote_ip`) über `s.reqLog(ctx)`. `handleLogout` loggt den Logout (Info) und ein fehlgeschlagenes Session-Invalidieren; `handleMe` loggt fehlgeschlagene User-Lookups. Die bisher stillschweigend verworfenen Fehler von `s.users.UpdateLastLogin(...)` und `s.authMgr.Logout(...)` (`_ =`) werden jetzt geprüft und geloggt. **Verifikation nach Deploy:** ein fehlgeschlagener Login (falsches Passwort) muss auf Level Info eine Warn-Zeile `login failed` **plus** `request rejected` mit identischer `request_id` erzeugen; ein erfolgreicher Login `login succeeded` mit `request_id`. **Ohne Go-Toolchain manuell gegengeprüfte Symbole** (für devops-deploy, falls der Build doch bricht): `auth.Manager.LoginFrom(ctx, id, pw, ip) (string, *userstore.User, error)` und `Logout(token) error` (`internal/auth/auth.go:88/330`), `userstore.Store.GetByID(int64) (*User, error)` und `UpdateLastLogin(int64) error` (`internal/userstore/userstore.go:132/387`), Felder `auth.Session{UserID, Username, TenantID}` und `userstore.User{ID, Username, TenantID}`, `s.reqLog(ctx) *slog.Logger`, `s.remoteIP(r) string`, `slog.Logger.Log(ctx, level, msg, args...)`. Keine neuen Imports nötig (`slog` in `observability.go` bereits vorhanden, `auth_handlers.go` unverändert bei `json`/`net/http`/`audit`). ## 2026-08-11 – FDN-08: Logging, Metriken & Fehler-Tracking **Zeit:** ca. 1,5 h (Ticket + bestehendes slog-Muster sichten, Middleware-Kette, Metrics-Endpunkt, Umstellung der Log-Call-Sites, Tests, Doku) **Ziel:** die drei realen Lücken schließen — Korrelations-ID über alle Schichten, `/metrics`, zentrales Panic-Recovery. Kein Loggerwechsel (`log/slog` bleibt), keine neue Fremdabhängigkeit. **Neu:** `internal/api/observability.go` (Request-ID-Middleware inkl. Übernahme/Sanitizing von `X-Request-ID`, `loggerFromCtx`/`s.reqLog(ctx)`, `statusRecorder`, `recoverMiddleware`, `metricsMiddleware`, `normalizeRoute`, prozesslokale `metricsRegistry` mit Latenz-Buckets), `internal/api/metrics_handlers.go` (`GET /metrics` im Prometheus-Textformat via `fmt.Fprintf`, IP-Beschränkung loopback + `api.metrics_allowed_ips`), `internal/api/observability_test.go` (je AK mindestens ein Test + Redaction-Test). **Geändert:** `internal/api/server.go` (Felder `metrics`/`baseHandler`, Kette `requestID -> metrics -> recover -> mux` in `New()` gebaut statt Lazy-Init in `ServeHTTP` — sonst Data Race; Route `GET /metrics`), `internal/storage/processing_jobs.go` (`CountProcessingJobsByStatus` für die Queue-Länge, bewusst ohne `tenant_id`-Filter, dokumentiert: einziger Aufrufer ist der aggregierte Metrik-Endpunkt), `config/config.go` + `config/config.yml.example` (`api.metrics_allowed_ips`), `cmd/archivdms/main.go` (Wiring), README-Abschnitt „Betrieb: Logging, Metriken & Fehler-Tracking (FDN-08)". **Kleinster Cut bei den Call-Sites:** statt jeden Aufruf umzuschreiben, gibt es `s.reqLog(ctx)`, das den Context-Logger nimmt und sonst auf `s.logger` zurückfällt. Alle 40 bisherigen `s.logger.*`-Aufrufe im Request-/Job-Pfad (`document_handlers.go`, `accounting_`, `public_share_`, `signed_url_`, `dashboard_`, `document_export_`, `document_bulk_export_`, `ocr_word_`) wurden mechanisch darauf umgestellt (`r.Context()` in Handlern, `ctx` in `ProcessDocumentJob`/`ReprocessDocument`/`archiveStagedFile`/`trySplitStagedUpload`/`autoAssignTaxonomy`/`generateThumbnailBestEffort`). `SetStorageConfig` bleibt bei `s.logger` (kein Request-Kontext). **Geheimnisschutz (Abnahme-Prüfung 2):** es wird nichts aus Query-String, Headern oder Body geloggt. `normalizeRoute` maskiert numerische IDs, hash-artige Segmente und immer das Segment hinter `/share/` — Share-Tokens können damit weder in Logs noch in Metrik-Labels auftauchen. Test `TestNormalizeRouteRedactsSecrets` deckt das ab. **Offen / auf 192.168.1.204 zu prüfen:** `go vet` + `go test ./internal/api/...` (hier kein Go-Toolchain), Scrape von `/metrics` (localhost = 200, fremde IP = 403), provozierter Panic → 500 + Logzeile mit `request_id` innerhalb einer Minute, Alarm-Schwelle in der Monitoring-Seite (Prometheus-Regel auf `archivdms_panics_total`/5xx-Rate) einmal auslösen und quittieren — Prüfung 1 und 3 der Kachel sind ohne laufende Instanz nicht abschließbar. ## 2026-08-11 – FDN-06: UI-Shell & Design-System (Abnahme) **Zeit:** ca. 0,5 h (Code-Review Shell/ui-Komponenten/Tokens, Doku-Lücke geschlossen) Reine Abnahme-Kachel, kein Neubau. Ergebnis der Prüfung: - **AK1 Shell steht:** erfüllt. `src/app/(app)/layout.tsx` setzt `SidebarProvider` → `AppSidebar` + `SidebarInset` → `TopBar` + Inhaltsbereich zusammen; `src/components/shell/` enthält AppSidebar (rollenabhängige Navigation), TopBar (Sidebar-Toggle, Suche, Theme-Umschalter, Benutzermenü), SearchBar, CommandPalette (Cmd+K). - **AK2 Basis-Komponenten:** Komponenten vollständig vorhanden (Table, Dialog, Sheet, Input/Textarea/Label/Calendar, Button, Badge, Card, Tabs, DropdownMenu/Popover/Command, Toast via sonner, Sidebar, Avatar/Progress/Skeleton); alle im Code verwendeten `@/components/ui/*`-Importe lassen sich auf existierende Dateien auflösen, keine fehlende Basiskomponente. **Lücke:** es gab keinerlei Doku dazu. Behoben durch neuen README-Abschnitt „Basis-Komponenten & Design-Tokens (FDN-06)" (Tabelle Komponente → Datei → Einsatzzweck). Kein Storybook — bewusst zu groß für diese Kachel. - **AK3 Design-Tokens:** erfüllt. HSL-Tokens zentral in `src/app/globals.css` (`:root`/`.dark`) inkl. `--radius` und `sidebar-*`, gemappt in `tailwind.config.ts`. Keine Hex-/RGB-Farbliterale in Komponenten (Prüfung per Suche); Inline-`style` nur für berechnete Geometrie (Progress, Cropper, OCR-Overlay). Einzige nicht-tokenisierte Farben sind semantische Statusfarben (emerald/amber/red) an Badges — akzeptiert und jetzt als Regel dokumentiert. Prüfungen vor der Abnahme: 1. **3 Breakpoints visuell:** *nicht durchführbar* (kein Dev-Server/Browser, `node_modules` nicht installiert) — manueller Check empfohlen. Ersatz-Code-Review: Sidebar wechselt über `useIsMobile` auf Sheet-Drawer, `ui/table.tsx` kapselt die Tabelle in `overflow-auto` (horizontal scrollbar statt Umbruch), Dialog/Sheet/Button/Calendar nutzen `sm:`-Varianten. Listenseiten selbst setzen kaum eigene Breakpoints — offener Punkt für `UX-01`. 2. **Tastaturbedienung:** durch Radix-Primitives abgedeckt (Dialog/Sheet, DropdownMenu, Popover, Tabs, Avatar, Progress) plus `cmdk` für die Befehlspalette — Fokus-Trap, Escape, Pfeiltasten, `aria-*` kommen aus der Bibliothek, nichts davon wurde überschrieben. Native `` im bestehenden shadcn-`Input`-Stil (kein neues Calendar-Widget eingeführt, da bislang keins im Projekt genutzt — hält Abhängigkeiten/Optik konsistent). Ändern schreibt sofort per `setDocumentDate` (`PUT /api/documents/{id}/document-date`), leeren löscht (`null`); zusätzlich ein `X`-Button zum expliziten Entfernen. Vorschlags-Chip aus `suggestion.document_date_candidate` wird NUR angezeigt, wenn noch kein `document_date` gesetzt ist, mit Score-Prozent und „Übernehmen“-Klick wie bei Tags (inkl. `markSuggestionReviewed`). **(2) Layout gestrafft:** Lose `
`-Blöcke ersetzt durch ein kompaktes 2-Spalten-Raster für die kurzen Felder (Belegdatum/Dokumenttyp/Korrespondent/Erstellt) — halbiert die Scroll-Strecke im üblichen Sichtungsfall. Titel, Akte und Tags bleiben volle Breite (können lang/mehrzeilig sein). Felder sitzen in leichten Karten-Rahmen (`rounded-md border bg-card/40`) mit kleinem Uppercase-Label, konsistent dark-mode-tauglich. **(3) Weniger Klicks:** Bestätigt, dass die Command-Popover-Picker (Dokumenttyp/Korrespondent/Akte/Tag) bereits direkt beim `onSelect` speichern — kein separater Apply-Schritt. Für das Datum ist die Übernahme ebenfalls Ein-Klick (onChange bzw. Chip). **(4) OCR-Kerninfo im Details-Tab:** Das aus dem Text erkannte Belegdatum wird direkt als „Erkannt“-Chip beim Datumsfeld sichtbar — kein Wechsel in den Inhalt-Tab nötig, um das zu prüfen/übernehmen. Bewusst minimal gehalten (Betrag o.ä. liegt nicht strukturiert vor → kein Overengineering). **Refactor:** Details-Tab-Logik (Titel/Datum/Typ/Korrespondent/Akte/Tags/Vorschläge, ~430 Zeilen) aus `DocumentPreview.tsx` (war 1118 Z.) in neue Komponente `DocumentDetailsTab.tsx` ausgelagert; `DocumentPreview` behält Layout/Resize/Fullscreen/Tabs/Reprocess/Verlauf/Inhalt. Beide Dateien jetzt deutlich unter 700 Z. **API-Typen (`src/lib/api.ts`):** `Document.document_date?: string | null` ergänzt; neue Fn `setDocumentDate(id, date|null)`; neues Interface `DateCandidate {date, score}`; `SuggestionPayload.document_date_candidate?: DateCandidate | null`. **Neue Dateien:** `src/components/documents/DocumentDetailsTab.tsx`. **Geänderte Dateien:** `src/lib/api.ts`, `src/components/documents/DocumentPreview.tsx`. **Build-Prüfung:** Kein lokaler `next build`/`tsc` möglich (kein `node_modules`). Typen manuell gegen bestehende Interfaces abgeglichen; Imports in DocumentPreview auf tatsächlich noch genutzte reduziert (Badge/Button/Tabs/RotateCw/Maximize2/Minimize2/useRouter/toast/getDocumentAuditLog/reprocessDocument). **Status:** Noch nicht deployed. **Projekt:** archivdms --- ## 2026-07-17 – Feature: Belegdatum (document_date) manuell editierbar + als Suggestion-Chip **Beschreibung:** Das bereits vorhandene, aus dem OCR-Text erkannte Beleg-/Rechnungsdatum (`documents.document_date`, Spalte + Extraktion `extractDocumentDate` + Auto-Set bei Upload/Reprocess waren schon live) wird jetzt (1) manuell setz-/löschbar und (2) als Vorschlags-Chip in der Suggestion-Pipeline angeboten. **(1) Manueller Endpunkt:** `PUT /api/documents/{id}/document-date` (`handleSetDocumentDate` in `internal/api/document_handlers.go`, Route in `server.go`). Body `{"document_date":"2024-12-31"}` setzt, `{"document_date":null}` (oder leerer String) löscht das Datum. Ownership-Check via `GetDocument(ctx,id,tenantID)` (WHERE tenant_id, kein IDOR). Nutzt bestehenden Store `UpdateDocumentDate` (verschiebt die WORM-Datei NICHT, nur Metadaten-Spalte). Audit: `EventDocumentUpdate` bei Erfolg UND Fehlschlag (invalid_format / not_found / update_failed). Antwort: aktualisiertes `Document`-JSON. **(2) Suggestion-Chip:** `SuggestionPayload` (`internal/storage/metadata_suggestions.go`) um optionales `document_date_candidate` (`*DocumentDateCandidate{Date string "YYYY-MM-DD", Score float64}`) erweitert. `GenerateHeuristicSuggestions` befüllt es NUR wenn das Dokument noch kein `document_date` hat und der OCR-Text ein plausibles Datum enthält; feste Confidence `heuristicDocumentDateScore = 0.7`. Extraktion `documentDateFromText` in neuer Datei `internal/storage/document_date.go` — bewusste Duplikat der api-Heuristik (storage darf internal/api nicht importieren, sonst Import-Zyklus; gleiche Duplikat-Praxis wie heuristicTitle vs. titleFromOCRText, byte-gleiche Regex). Frontend kann das Datum via obigem PUT übernehmen. **Neue Dateien:** `internal/storage/document_date.go`. **Geänderte Dateien:** `internal/api/document_handlers.go` (Handler + Request-Typ), `internal/api/server.go` (Route), `internal/storage/metadata_suggestions.go` (Payload-Feld + Befüllung). **Signatur-Prüfung (kein lokaler go build – go in Sandbox nicht installiert):** manuell gegen echte Definitionen geprüft: `Store.UpdateDocumentDate(ctx, id, tenantID int64, date *time.Time) error` (documents.go:396) — nil löscht; `Store.GetDocument(ctx,id,tenantID) (*Document,error)`; `Document.DocumentDate *time.Time json:"document_date,omitempty"` (schon vorhanden, GET liefert es bereits); `audit.EventDocumentUpdate = "document_update"` (audit.go:31, bisher ungenutzt, passt); `audit.Entry`-Felder (EventType/Username/TenantID/DocumentID/Success/Detail) wie in Nachbar-Handlern. `SuggestionPayload` neues Feld ist Pointer+omitempty → die 3 anderen Konstruktionsstellen (ollama/naivebayes/scan) brechen nicht (default nil). document_handlers.go importiert `time`/`strings`/`errors`/`encoding/json` bereits. document_date.go importiert regexp/strconv/time. devops-deploy: falls Build rot, zuerst UpdateDocumentDate-Signatur und EventDocumentUpdate-Konstante prüfen. **Frontend-Übergabe:** JSON-Feld am Dokument = `document_date` (ISO `YYYY-MM-DD`, fehlt/null wenn ungesetzt). Setzen/Löschen: `PUT /api/documents/{id}/document-date` Body `{"document_date":"YYYY-MM-DD"|null}`. Suggestion-Feld im Heuristik-Payload: `suggestion.document_date_candidate` (`{date, score}`). **Status:** Noch nicht deployed. **Projekt:** archivdms --- ## 2026-07-17 – Feature: ML-Klassifizierung Phase 5 (Frontend) – Provider-Badge + "Warum vorgeschlagen?"-Popover an Vorschlags-Chips **Beschreibung:** Erweitert die bestehende Vorschlags-UI in der Dokumentvorschau (`DocumentPreview.tsx`, Reiter „Details“) um Provider-Transparenz für den neuen `naive_bayes`-Provider (Phase 2-4). **(1) Provider-Badge:** Jeder Vorschlagslauf hat genau einen Provider — neben jeder „Vorschläge:“/„Vorschlag:“-Zeile (Titel/Tags/Dokumenttyp/Korrespondent) erscheint nun ein kleines beschriftetes `Badge` (`variant="secondary"`, sprechendes Label statt Icon: `heuristic`→„heuristisch“, `ollama`→„KI“, `naive_bayes`→„gelernt“; unbekannte Provider fallen auf den Rohwert zurück). Ein wiederverwendetes JSX-Element `providerBadge`, kein Duplizieren. **(2) Erklärungs-Popover:** Bei `provider === 'naive_bayes'` und vorhandenem `explanation`-Array (Top-Tokens der Klassenentscheidung) steht neben jedem Kandidaten-Chip ein `Info`-Icon-Button (`lucide-react`), der ein `Popover` „Warum vorgeschlagen?“ mit den ausschlaggebenden Begriffen als Badge-Liste öffnet — GoBD-Nachvollziehbarkeit. Der Popover-Trigger steht NEBEN dem Chip-Button (eigenes ``), nicht darin (kein `