# PROJ-58: Indexierung + OCR als Cron-Batch-Jobs (statt Dauerbetrieb)

## Status: Deployed
**Created:** 2026-06-24
**Last Updated:** 2026-06-24

## Dependencies
- PROJ-30 (Manticore-Indexierung)
- PROJ-35 (OCR & Anhang-Volltext-Indexierung)
- PROJ-56 (Last-Entzerrung für Hintergrundjobs — verwandtes Cron-Muster)

## Hintergrund (Nutzerwunsch)
Aktuell laufen Indexierung (`internal/index/tenant_worker.go`) und OCR (`internal/ocr/worker.go`) sofort und nebenläufig bei jedem Mail-Import als Dauerbetrieb-Goroutinen. Das erzeugt viele kleine Schreibzugriffe auf die Festplatte (Manticore-Writes, OCR-Tesseract-Output, DB-Updates) statt geblockter Batches. Nutzerwunsch: Beide Prozesse sollen optional in größeren, **per `cron.d` konfigurierbaren Zeitabständen** laufen, damit die Zeiten später selbst angepasst werden können (analog zum bestehenden Purge-Cron, PROJ-56c).

## Bestehende Bausteine (bereits vorhanden, lt. Code-Analyse)
- `indexed_at TIMESTAMPTZ` (storage.go) markiert bereits indexierte Mails — Query auf `indexed_at IS NULL` liefert die Pending-Liste ohne neue Spalte.
- `ocr_status` (pending/done/failed/skipped/disabled) ist bereits vollständig vorhanden.
- `cmd_ocr_reprocess.go` ist bereits "lade Batch → verarbeite → beenden" und damit direkt cron-fähig.
- Es fehlt ein äquivalenter Batch-Befehl für die Indexierung (aktuell nur `cmd_reindex.go`, das *immer alle* Mails neu indexiert statt nur die ungeindexten — ungeeignet für einen häufigen Cron-Lauf).

## Entscheidung (Nutzer, 2026-06-24)
- Modus per Config umschaltbar (`index.batch_mode`, `ocr.batch_mode`), Default `false` = aktuelles Verhalten unverändert (non-breaking, analog PROJ-56).
- Bei `batch_mode: true` wird der jeweilige Dauerbetrieb-Worker im Daemon **nicht gestartet**; neue Mails bleiben bis zum nächsten Cron-Lauf mit `indexed_at IS NULL` / `ocr_status='pending'` in der DB stehen (kein Datenverlust, nur verzögerte Sichtbarkeit in Suche/OCR).
- Zeiten stehen in `/etc/cron.d/archivmail`, frei editierbar, mit demselben Kommentarstil wie der bestehende OCR-Pausen- und Purge-Cron.

## Acceptance Criteria
- [x] Neuer CLI-Befehl `archivmail index-pending --config ... --limit N` lädt Mails mit `indexed_at IS NULL` (Query-Pattern analog `cmd_ocr_reprocess.go`), indexiert sie über den `TenantIndexWorker`, wartet auf vollständiges Drain, beendet sich danach.
- [x] `config.yml`: neue Felder `index.batch_mode` (bool, default false) und `ocr.batch_mode` (bool, default false).
- [x] Bei `batch_mode: true` wird der jeweilige Worker beim Daemon-Start nicht gestartet und der Upload-Pfad submitted nicht mehr in den In-Memory-Channel (kein sinnloses Queue-Volllaufen/Log-Spam).
- [x] Bei `batch_mode: false` (Default) bleibt das bisherige Verhalten 1:1 erhalten — keine Regression für bestehende Installationen.
- [x] `deploy/cron.d/archivmail` bekommt zwei neue, kommentierte Cron-Zeilen für `index-pending` und `ocr-reprocess --status pending`, mit Beispiel-Intervall (z.B. alle 15 Minuten), klar als "Zeiten hier anpassen" markiert — analog zum bestehenden OCR-Pausenfenster-Kommentarstil.
- [x] Boot-Resume-Goroutinen (OCR-Backfill in main.go, Index-Backfill `runBackfill`) laufen nur, wenn der jeweilige `batch_mode` **nicht** aktiv ist (sonst übernimmt der Cron-Job diese Aufgabe).
- [x] Dokumentation im Cron-File erklärt, dass bei `batch_mode: true` neue Mails erst nach dem nächsten Cron-Lauf durchsuchbar/OCR-bearbeitet sind.

## Tech Design
Übersprungen (klar umrissene, additive Konfigurationsoption mit bestehenden Bausteinen — kein architektonischer Schnitt, analog PROJ-56).

## Implementation Notes (2026-06-24)

### Geänderte/neue Dateien
- `config/config.go`: neues Feld `BatchMode bool` (`yaml:"batch_mode"`, default false) in `IndexConfig` und `OCRConfig`. Additiv/non-breaking, kein Pointer nötig da `false` der gewünschte Default ist.
- `internal/storage/storage.go`: neue Funktion `GetUnindexedMails(ctx, limit)` + Typ `UnindexedMail{ID, TenantID}` — Query `WHERE indexed_at IS NULL ORDER BY received_at DESC` (analog zu `GetMailsByOCRStatus`).
- `cmd/archivmail/cmd_index_pending.go` (neu): CLI-Befehl `index-pending` (Flags `--config`, `--limit`), Vorbild `cmd_ocr_reprocess.go`. Lädt ungeindexte Mails, parst sie, baut `index.MailDocument`, queued sie auf einen frisch erstellten `TenantIndexWorker` (Queue = batch+16, kein Drop), setzt `indexed_at`, wartet via `worker.Stop()` auf vollständiges Drain, beendet sich.
- `cmd/archivmail/main.go`: Befehl im Dispatcher registriert. Daemon-Start gated: bei `cfg.Index.BatchMode` kein `tenantWorker.Start()` und kein `runBackfill`; bei `cfg.OCR.BatchMode` kein `ocrWorker.Start()`, keine OCR-Boot-Resume-Goroutine, keine IMAP/POP3-`SetOCRSubmit`-Hooks. `submitToWorker()` um zwei Flags (`indexBatchMode`, `ocrBatchMode`) erweitert → überspringt die jeweiligen In-Memory-Submits (kein Queue-Volllaufen / Log-Spam). Mails behalten dabei `indexed_at IS NULL` bzw. `ocr_status='pending'` und werden vom Cron-Job nachgezogen.
- `deploy/cron.d/archivmail`: zwei neue, kommentierte Zeilen (`index-pending` `*/15`, `ocr-reprocess --status pending` `5,20,35,50`), klar als nur-bei-`batch_mode:true`-relevant und frei editierbar markiert.
- `config/config.docker.yml.example`: auskommentierte Beispiele für `index.batch_mode` und `ocr.batch_mode`.

### Design-Entscheidungen / Abweichungen
- `BatchMode` ist ein einfacher `bool` (kein Pointer wie bei PROJ-56 `JitterSeconds`), da hier `false` = Default = gewünschtes Alt-Verhalten; eine Unterscheidung unset/explizit-false ist nicht nötig.
- IMAP/POP3-Importer indexieren synchron direkt über `idxMgr` (nicht über den `TenantIndexWorker`) — dieser Pfad bleibt unverändert; `index.batch_mode` betrifft bewusst nur den Worker-/SMTP-Upload-Pfad (Schreiblast-Glättung des Async-Workers). OCR-Hooks der Importer werden hingegen bei `ocr.batch_mode` deaktiviert, da OCR ausschließlich über den Worker läuft.
- Lokal kein `go build` möglich (kein Toolchain) — nur statische Konsistenzprüfung; Build-Verifikation auf 192.168.1.131/132.

## QA Test Results (192.168.1.132, 2026-06-24)
- Build: `CGO_ENABLED=0 go build -buildvcs=false -o archivmail ./cmd/archivmail/` → Exit 0.
- `go vet ./...`: nur vorbestehende, PROJ-58-unabhängige Befunde (xapian_wrapper.cpp/cgo, storage.go self-assignment, storage_test.go-Signatur). Keine neuen Befunde durch PROJ-58.
- Default-Verhalten (`batch_mode` unset/false): Live-Service unverändert weitergelaufen, kontinuierliche Indexierung/OCR + Boot-Backfill bestätigt aktiv, `/api/health` ok.
- `batch_mode: true` (isolierte Test-Config): Daemon startet, loggt "batch mode enabled — continuous worker not started" für beide Worker, kein Backfill/Boot-Resume, keine kontinuierliche Verarbeitung — wie spezifiziert.
- `index-pending --limit 5` gegen Test-DB: 5 ungeindexte Mails geladen, Worker drained, Exit 0.
- Minor-Finding behoben: `index-pending` fehlte in `printHelp()` (cmd_import.go) — ergänzt.
- Keine Critical/High-Findings. Server 192.168.1.131 nicht angefasst während der QA.

## Nachtrag (2026-06-24): Lockfile-Schutz gegen überlappende Cron-Läufe
Nutzer wies auf mailpiler-Vorbild hin (`indexer.delta.sh`, PID-Lockfile-Muster), um zu verhindern, dass sich Cron-Läufe bei großem Backlog überlappen (würde die Last-Glättung wieder aufheben). Umgesetzt:
- Neue Wrapper-Skripte `deploy/cron.d/archivmail-index-pending.sh` und `deploy/cron.d/archivmail-ocr-reprocess.sh` — Lockfile unter `/var/run/archivmail/*.lock`, analog zu Pilers `MAINTMPFILE`/`DELTATMPFILE`-Muster (Datei mit Zeitstempel statt PID, da `trap ... EXIT` zuverlässig aufräumt; bei bereits laufendem Lauf wird einfach übersprungen statt zu warten/abzubrechen mit Fehlercode, damit Cron keine Fehlermail wegen "schon belegt" verschickt).
- `deploy/cron.d/archivmail` ruft jetzt die Wrapper-Skripte unter `/usr/local/bin/` auf statt das Binary direkt.
- **Root Cause für "Last geht nicht runter" gefunden:** `update.sh` synct `/etc/cron.d/archivmail` nie auf den Server — die PROJ-58-Cronzeilen fehlten auf 131 UND 132 komplett, obwohl `batch_mode` im Code bereits aktiv war. Daher lief weiterhin nichts batch-weise, weil der Backlog nie per Cron abgeholt wurde. Fix: `update.sh` kopiert jetzt `deploy/cron.d/archivmail` nach `/etc/cron.d/archivmail` und alle `deploy/cron.d/*.sh`-Wrapper nach `/usr/local/bin/` bei jedem Deploy.

## Nachtrag (2026-06-24): batch_mode als Default für Neuinstallationen
Auf Nutzerwunsch ist `index.batch_mode: true` und `ocr.batch_mode: true` jetzt aktiv (nicht mehr auskommentiert) in `config/config.docker.yml.example` gesetzt, damit neue Installationen direkt mit Cron-Batch statt Dauerbetrieb starten. Bestehende Installationen (wie 192.168.1.131/132) sind davon nicht betroffen, da deren `/etc/archivmail/config.yml` unabhängig vom Repo-Beispiel ist und weiterhin ohne `batch_mode`-Einträge (= Dauerbetrieb) läuft.

## Out of Scope
- Kein Wechsel der bestehenden Mechanismen für Installationen, die `batch_mode` nicht setzen.
- Kein UI/Admin-Schalter im Frontend — Konfiguration ausschließlich über `config.yml` + `cron.d`.
