Files
pdf-ocr-hotfolder/CHANGELOG.md
T
techadmin 465ff8873f fix: Ghostscript-Angebot log nicht mehr, fehlende Voraussetzungen dokumentiert (v0.7.1)
Befunde aus dem ersten echten Erstinstallations-Test auf frischen Debian-12-
und Debian-13-Containern. Der Weg selbst hat getragen (Basis-Install,
Instanz-Anlage, zweite Instanz, Update mit Rauchtest, Rollback) — diese
Stellen haben gelogen oder gefehlt:

- Das Ghostscript-Backports-Angebot auf Debian 12 war ein garantierter
  Leerlauf, der "aktualisiert ✓" meldete: bookworm-backports enthaelt gar
  kein ghostscript (am Paketindex verifiziert). Die Routine sucht jetzt den
  echten Kandidaten, vergleicht vorher/nachher und raeumt eine nur zur Probe
  angelegte Quelle wieder weg.
- Derselbe untaugliche Rat stand in der Preflight-Meldung, der
  pdfa_level-Warnung, config.example.toml und vier Doku-Dateien — ueberall
  ersetzt durch die echten Optionen.
- Mehrzeilige Log-Hinweise waren durch "echo -e" zerrissen und nicht
  kopierbar; log_* nutzt jetzt printf mit %s.
- pip-freeze.txt landete beim Rollback als /pip-freeze.txt im
  Wurzelverzeichnis, liegt jetzt unter opt/pdf-ocr-hotfolder/.
- git und sudo fehlen auf dem Proxmox-Debian-Template; "sudo ./install.sh"
  scheitert dort. Beide Wege dokumentiert, git als Voraussetzung ergaenzt,
  HTTPS-Clone als Normalfall.
- Rollback: systemctl start kann kein Glob. journald-Reparatur: Instanzen
  danach neu starten, sonst bleibt das Journal leer.

254 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 01:36:42 +02:00

732 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## [0.7.1] - 2026-09-23
Gefunden beim ersten echten Erstinstallations-Test auf frischen Debian-12-
und Debian-13-Containern. Der Installationsweg selbst hat getragen; diese
drei Stellen haben gelogen oder gefehlt.
### Fixed
- **Das Ghostscript-Angebot auf Debian 12 war ein No-Op, der sich als Erfolg
meldete** (`Ghostscript aktualisiert: 10.00.0 -> 10.00.0 ✓`). Grund:
`bookworm-backports` enthaelt ueberhaupt kein `ghostscript` — am echten
Paketindex verifiziert (2606 Pakete, ghostscript nicht dabei, Kontroll-
pakete wie systemd/golang-go sehr wohl). Zurueck blieb eine nutzlose
`sources.list.d`-Quelle. Die Routine (jetzt `check_ghostscript` in
`lib/common.sh`) sucht nun erst den tatsaechlichen Kandidaten, vergleicht
die Version vorher/nachher, meldet "unveraendert" als Warnung statt als
Erfolg und entfernt eine nur zur Probe angelegte Quelle wieder. Eine
bereits vorhandene Backports-Quelle bleibt unangetastet.
- **Der Rat "Ghostscript aus bookworm-backports" ist ueberall raus** — er
stand auch in der Preflight-Fehlermeldung, der pdfa_level-Warnung,
config.example.toml und vier Doku-Dateien. Ersetzt durch die echten
Optionen: pdfa_level leer lassen (Default), skip_text = false, oder
Debian 13 (gs 10.05.1). Ein Test prueft negativ, dass der Rat nicht
zurueckkommt.
- **Mehrzeilige Log-Hinweise waren zerrissen und nicht kopierbar**: die
log-Funktionen nutzten `echo -e`, wodurch `\n` in Hinweistexten zu echten
Zeilenumbruechen ohne `[WARN]`-Praefix wurden. Jetzt `printf` mit `%s`
fuer den Text — das Problem kann strukturell nicht wiederkommen.
- **`pip-freeze.txt` landete beim Rollback im Wurzelverzeichnis.** Sie liegt
im Backup jetzt unter `opt/pdf-ocr-hotfolder/` und damit nach dem
Entpacken neben der Installation.
### Added (Doku)
- Voraussetzungen, die auf einem frischen Proxmox-Debian-Template fehlen:
**`git` und `sudo`** sind dort nicht installiert — der dokumentierte
Aufruf `sudo ./install.sh` scheitert mit `sudo: command not found`.
Beide Wege (root direkt / sudo) sind jetzt beschrieben.
- HTTPS- statt SSH-Clone als Normalfall (funktioniert ohne Credentials);
SSH-Variante mit dem Hinweis, dass der User `gitea` heisst, nicht `git`.
- Entscheidungstabelle zu PDF/A auf Debian 12 vs. 13. Praezisierung: die
Sackgasse ist PDF/A **zusammen mit** `skip_text = true`; mit
`skip_text = false` geht PDF/A auch auf Debian 12, nur langsamer.
- Rollback: `systemctl start` kann kein Glob (anders als `stop`), bei
mehreren Instanzen jede einzeln starten. Dazu, was ein Rollback
nachweislich zurueckholt und was nicht.
- journald-Reparatur: die Instanzen danach einmal neu starten, sonst bleibt
das Journal leer und die Reparatur sieht gescheitert aus.
- "So sieht ein Erstlauf aus" — die Reihenfolge der Abfragen.
## [0.7.0] - 2026-09-23
Schliesst die stillen Datenverlust-Pfade: gleichnamige Dateien werden nirgends
mehr ueberschrieben, und ein nicht aufrufbares veraPDF wird nicht mehr als
"PDF ist ungueltig" missverstanden. Dazu Robustheit (Exit 2 statt Traceback,
toter Verzeichnis-Watch faellt auf, relative Pfade sind ein Config-Fehler) und
eine gemeinsame Shell-Bibliothek fuer install.sh und update.sh.
Test-Suite: 254 pytest-Tests gruen.
### Added
- **`lib/common.sh` — gemeinsame Shell-Bibliothek.** `install.sh` und
`update.sh` sourcen sie und teilen sich darueber Log-Funktionen
(`log_info`/`log_warn`/`log_error`/`log_step`), `require_root`, die
Layout-Konstanten (`INSTALL_DIR`, `CONFIG_DIR`, `DATA_ROOT`, `SYSTEMD_DIR`,
`DEFAULT_USER`, `SERVICE_TEMPLATE`, `LXC_DROPIN*`), die apt-Paketliste
(`pdf_ocr_apt_packages()` zwischen den BEGIN/END-Marken) und die
venv-Pruefung (`py_mm`, `pyvenv_cfg_mm`, `venv_is_healthy`,
`report_venv_issues`).
- Die **Paketliste** steht damit nicht mehr in `install.sh`, sondern in
`lib/common.sh`; `update.sh` schneidet sie weiterhin per `sed` aus der
**Repo**-Fassung heraus, damit beim Update die neue Liste gilt und nicht
die vielleicht aeltere, bereits gesourcte aus der Installation.
- `venv_is_healthy()` gab es bisher **zweimal** — die schlanke Variante in
`install.sh` haette den Distro-Upgrade-Fall (`pyvenv.cfg` gegen
System-Python) nicht erkannt. Jetzt existiert nur noch die gruendliche
Fassung, und `install.sh` nennt die Befunde ueber `report_venv_issues`.
- `lib/` wird von `install.sh` **und** `update.sh` nach
`/opt/pdf-ocr-hotfolder/lib/` mitkopiert und liegt damit im
Update-Backup. `update.sh` sourct bevorzugt die Repo-Fassung neben sich
und faellt auf die installierte zurueck; fehlt sie ueberall, bricht es
sofort ab statt mitten im Lauf mit "command not found".
- **veraPDF wird im Preflight geprueft** (`check_verapdf_binary()`,
`resolve_verapdf_binary()`). Mit `[verapdf].enabled = true` muss
`[verapdf].binary` auf ein vorhandenes, ausfuehrbares Programm zeigen —
sonst startet der Dienst gar nicht erst (Exit 2), und `--check-config`
meldet den Fehler. Ein Pfad mit `/` wird direkt geprueft, ein nackter Name
im `PATH` gesucht. `--check-config` zeigt jetzt ausserdem Binary und Flavour
bzw. `(aus)` an.
Hintergrund: ein Tippfehler im Pfad war der **gefaehrlichste Fehler des
ganzen Dienstes**. `run_verapdf()` fand das Programm fuer JEDE Datei nicht,
wertete das als FAIL, schob das OCR-Ergebnis nach `error/` — und
`_dispose_original()` entsorgte das Original laut
`[output].original_on_success`, bei dessen Default `delete` also Scan fuer
Scan die Vorlage. Die Unit stand dabei als `active (running)` da.
- **Exit 3: toter Verzeichnis-Watch** (`EXIT_OBSERVER_DEAD` in `service.py`).
Stirbt der watchdog-Observer im Betrieb (erschoepftes
`fs.inotify.max_user_watches`, ersetztes oder neu gemountetes Verzeichnis),
blieb die Unit bisher `active (running)` und verarbeitete nichts mehr — kein
Log, keine Mail, niemand merkt es. Die Hauptschleife prueft den Observer
jetzt sekuendlich mit, loggt im Ernstfall die moeglichen Ursachen und
beendet sich mit Exit 3; `Restart=on-failure` startet den Dienst neu und der
Watch wird neu aufgesetzt. Bewusst **nicht** Exit 2 — den unterdrueckt die
Unit jetzt beim Neustart (s.u.).
- **`ProcessResult.warning`** — erfolgreicher Durchlauf mit Nebenbefund. Bisher
gab es nur Erfolg oder Fehler; ein liegengebliebenes Original passte in
keine der beiden Schubladen.
- **Sammelmeldung fuer Nicht-PDF-Dateien in `incoming/`**
(`_report_non_pdf()`). Alles ohne `.pdf`-Endung wurde ignoriert und
sammelte sich stumm an (Scanner-Fehlablagen, abgebrochene Uploads,
Thumbnails). Beim Start-Scan gibt es jetzt **eine** Warnung mit Anzahl und
bis zu drei Beispielnamen — keine Zeile pro Datei und nichts im laufenden
Betrieb.
- **`install.sh` warnt beim Erstinstall in Containern, wenn
`systemd-journald` nicht laeuft.** Der Dienst loggt ausschliesslich nach
journald; ist journald kaputt, gibt es gar keine Logs. Die Warnung nennt den
Drop-in-Befehl fuer den Debian-13-Fall und fragt, ob fortgefahren werden
soll.
- **Dokumentation** (siehe unten unter *Docs*): Dateisystem-Festlegung,
Debian-13-journald-Befund, konkrete Speicher-Messwerte, Exit-Code-Tabelle.
### Fixed
- **`outgoing/`: gleichnamige Datei wird nicht mehr ueberschrieben.** Liefert
der Scanner denselben Dateinamen ein zweites Mal (oder wurde das
Vorgaengerergebnis noch nicht abgeholt), legte der `shutil.move` die
aeltere Datei kommentarlos um. Neu: `_collision_free_path()` haengt einen
Zeitstempel an (`scan.pdf` -> `scan_20260923-081500.pdf`), bei Kollision
innerhalb derselben Sekunde zusaetzlich einen Zaehler. Es gibt eine
`log.warning`, und `ProcessResult.output` traegt den **tatsaechlich**
geschriebenen Pfad — die Upload-Ziele und die Mail nennen damit die richtige
Datei.
- **Dieselbe Klasse in `error/` und beim Ordner-Upload.** `_move_to_error()`
(und damit auch `_rescue_to_error()`) sowie `upload_folder()` mit
abweichendem `[upload.folder].target` ersetzten bisher still eine
gleichnamige Datei im Ziel. Beide nutzen jetzt denselben
Zeitstempel-Ausweg. Scheitert dieselbe `scan.pdf` zweimal, liegen jetzt
beide Fassungen in `error/`.
- **veraPDF: Stoerung wird nicht mehr als FAIL gewertet.** `run_verapdf()`
unterscheidet jetzt zwischen einem echten Urteil (PASS/FAIL) und
"Programm nicht aufrufbar, nicht startbar, Timeout oder kein PASS/FAIL in
der Ausgabe" — Letzteres wirft `VeraPdfUnavailable`. Im Stoerungsfall
wandern **Original UND OCR-Ergebnis** nach `error/`, und das Original wird
weder geloescht noch archiviert, unabhaengig von
`[output].original_on_success`. Vorher lieferte jeder dieser Faelle
schlicht `False` und damit ein Fehlurteil ueber die Datei.
- **Kaputtes TOML und nicht lesbare Config beenden den Dienststart mit
Exit 2** statt mit einem nackten Traceback. `main()` faengt jetzt
`tomllib.TOMLDecodeError` und `OSError` genauso ab wie `--check-config`; die
Meldung nennt Zeile und Spalte, sofern der Interpreter sie liefert
(`TOMLDecodeError.lineno`/`.colno` gibt es erst ab Python 3.14 — auf
Debian 12 steht die Position nur im Meldungstext, deshalb `getattr`).
- **Relative Pfade in der Config sind jetzt ein Fehler.** `[paths].incoming`,
`outgoing`, `working`, `error` sowie `[output].archive_dir` und
`[upload.folder].target` muessen absolut sein (`_require_absolute()`,
`ConfigError` + Exit 2). Ein relativer Pfad wurde gegen das
`WorkingDirectory` der Unit aufgeloest und landete still unter
`/opt/pdf-ocr-hotfolder/` — der Scanner schrieb dann woanders hin als der
Dienst schaute, ohne dass irgendwo ein Fehler auftauchte. Leere Werte
bleiben erlaubt (`archive_dir`/`target` sind optional).
- **Ein Fehler beim Entsorgen des Originals entwertet den Durchlauf nicht
mehr.** `_dispose_original()` wirft nicht mehr, sondern liefert eine
Meldung zurueck: zum Aufrufzeitpunkt liegt das fertige PDF schon in
`outgoing/`, eine Exception von dort haette den gelungenen Durchlauf im
Catch-all des Service in einen Fehler verwandelt — mitsamt ausgefallenem
Upload. Jetzt gilt der Lauf als Erfolg, Upload und Benachrichtigung laufen,
es gibt aber eine `log.error` (Original liegt noch in `working/` und wird
beim naechsten Start erneut durch das OCR geschickt), und die Mail geht als
**"OK mit Warnung"** auch bei `[notify.email].on = "errors"` raus — sonst
waere genau das wieder ein stiller Fehlerpfad.
- **Log geht explizit nach stdout.** `logging.basicConfig()` schreibt per
Default nach **stderr**; README und `docs/INSTALLATION.md` versprachen aber
stdout. Fuer journald egal, fuer den dort beschriebenen
Vordergrund-Notbehelf und fuer jede Weiterleitung nicht.
### Changed
- **`systemd/pdf-ocr-hotfolder@.service`: `RestartPreventExitStatus=2`.**
Exit 2 = Config- oder Preflight-Fehler, den behebt kein Neustart. Bisher
startete `Restart=on-failure` die Instanz endlos im 5-Sekunden-Takt neu
(das Start-Rate-Limit greift bei `RestartSec=5` nie). Jetzt bleibt die
Instanz sichtbar `failed` stehen.
- `HotfolderService.run()` gibt einen **Exit-Code** zurueck (0 oder
`EXIT_OBSERVER_DEAD`) statt `None`; `main()` reicht ihn durch. Preflight und
`check_output_config()` stehen jetzt gebuendelt in `_preflight()`, das
`run()` und `run_once()` gemeinsam nutzen.
- `check_preflight()` nimmt zwei weitere Parameter (`verapdf_enabled`,
`verapdf_binary`) — beide mit Default, bestehende Aufrufe bleiben gueltig.
- `config.example.toml`: Warnhinweise zu absoluten Pfaden (`[paths]`,
`[output].archive_dir`, `[upload.folder].target`), zur veraPDF-Preflight-
Pruefung samt Begruendung und zum Kollisionsverhalten im Upload-Ziel.
- `install.sh` ist von ~73 Zeilen Kopf auf das Sourcen von `lib/common.sh`
geschrumpft und nutzt durchgehend `SYSTEMD_DIR`/`LXC_DROPIN` statt
hartkodierter Pfade.
### Docs
- **`docs/INSTALLATION.md`, Systemanforderungen: Dateisystem festgelegt.** Der
Dienst laeuft ausschliesslich auf **ext4, xfs oder zfs**. `incoming/` gehoert
auf ein lokales, inotify-faehiges Dateisystem — **kein CIFS/NFS-Mount**: dort
liefert inotify grundsaetzlich keine Events, weil Schreibzugriffe anderer
Rechner am lokalen Kernel vorbeigehen. Der Dienst wuerde dann nur noch beim
Start etwas verarbeiten und im laufenden Betrieb nichts mehr bemerken.
- **Speicher-Messwerte konkretisiert.** Zur bestehenden 512-MB-Messung kommen
Zahlen von einem **2-GB-Container** (Debian 13, 300-dpi-A4-Seite mit
`deskew`, `oversample = 300`, `jobs = 4`): Laufzeit **19 s**, `MemoryPeak`
des Dienstes **380 MB**, `memory.peak` des ganzen Containers **503 MB**,
`oom_kill 0`. Auf derselben Maschine mit 512 MB war genau das der OOM-Kill.
Die Empfehlung "mindestens 2 GB" ist damit belegt statt geschaetzt.
- **Debian 13 in LXC auf Proxmox: journald scheitert — verifizierter Befund,
betrifft jede Debian-13-LXC auf Proxmox 8.4.** In 0.6.3 stand noch, das sei
ein Schaden auf genau einer Maschine; das war falsch. Ursache: systemd >= 255
(Debian 13 hat 257) setzt `ImportCredential=journal.*` in der
journald-Unit, der Hilfsprozess `(sd-mkdcreds)` mountet dafuer, und das
AppArmor-Profil des Proxmox-Hosts blockiert das
(`apparmor="DENIED" operation="mount" profile="lxc-<id>_</var/lib/lxc>"
name="/dev/" comm="(sd-mkdcreds)"` im Host-Log). Debian 12 (systemd 252)
kennt `ImportCredential` nicht und ist nicht betroffen. Dokumentiert sind
die reboot-feste Abhilfe im Container (Drop-in
`systemd-journald.service.d/no-credentials.conf` mit leerem
`ImportCredential=`), der Hinweis auf die weiteren betroffenen Units
(logind, networkd, console-getty, tmpfiles-setup) und der saubere Weg
host-seitig.
- **Exit-Code-Tabelle 0/1/2/3** in `docs/INSTALLATION.md`, verlinkt aus
README, `docs/UPDATE.md` und dem Troubleshooting — mit dem Hinweis, dass die
Unit bei 2 **nicht** neu startet und bei 3 gerade doch.
- Die fuenf Ueberschriften der Instanz-Abfragen in `docs/INSTALLATION.md` sind
**entnummeriert** (`### 4. OCR-Sprachen` -> `### OCR-Sprachen`). Die Anker
hiessen vorher `#4-ocr-sprachen` und waeren bei jeder Umsortierung
gebrochen; die Reihenfolge steht weiterhin im Text.
- `AI_AGENT_BRIEFING.md` auf den heutigen Stand gezogen: Dateibaum mit `lib/`
und allen 20 Test-Dateien, nur noch **ein** `venv_is_healthy()`,
Paketliste in `lib/common.sh`, veraPDF-Preflight, Kollisionsschutz,
Exit-Codes, Observer-Bewachung, Testzahl 254.
- Testzahl ueberall von 152 auf **254** korrigiert (README,
`AI_AGENT_BRIEFING.md`, `docs/OS-UPGRADE.md`).
## [0.6.3] - 2026-09-23
Reine Doku-Version — kein Code, kein Installer, kein Updater, keine Unit, keine
Tests angefasst (ausser dem Versionsstring). Beide Erkenntnisse stammen aus den
Testlaeufen auf Debian 12 und Debian 13.
### Added
- **Abschnitt "Systemanforderungen" in `docs/INSTALLATION.md`.** Die
Dimensionierung fehlte bisher komplett. Empfohlen werden **mindestens 2 GB
RAM**; 512 MB reichen fuer 300-dpi-Scans nachweislich nicht. Gemessen auf
einem LXC-Container mit 512 MB RAM + 512 MB Swap, Debian 13,
Ghostscript 10.05.1: eine **einzelne A4-Seite in 300 dpi** mit
`deskew = true` riss das cgroup-Limit und der Dienst wurde vom OOM-Killer
beendet (`oom-kill:constraint=CONSTRAINT_MEMCG, oom_memcg=/lxc/201,
task=python`, `total-vm:1168764kB, anon-rss:489676kB`). Dieselbe
Verarbeitung mit einer kleineren Seite (850x1100 px) lief in ca. 19 s sauber
durch.
- Dokumentiert ist der Zusammenhang **RAM <-> `max_workers` x `jobs` x
Aufloesung**: `max_workers` (Default 2) laesst zwei solcher Seiten
gleichzeitig laufen, der Spitzenbedarf multipliziert sich entsprechend.
Wer knapp dimensioniert, zieht zuerst `max_workers` herunter.
- Dazu, **wie sich ein OOM-Kill aeussert** (Dienst weg bzw. von systemd neu
gestartet, `NRestarts` steigt, Abbruch mitten in der Datei ohne Traceback,
PDF bleibt in `working/` liegen) und **wie man ihn nachweist**
(`/sys/fs/cgroup/memory.events` im Container, `dmesg` auf dem LXC-Host).
Ohne diese Pruefung sieht der Fall wie ein Anwendungsfehler aus und man
sucht in ocrmypdf, Tesseract oder der Config.
- Mit dem Hinweis, dass die Wiederaufnahme aus `working/` seit v0.6.0 den
Datenverlust abfaengt — der OOM selbst bleibt aber ein Problem: bei
unveraenderter Dimensionierung laeuft dieselbe Datei nach dem Neustart
erneut hinein.
- `README.md` bekommt im Schnellstart nur einen Einzeiler mit Verweis, keine
Dublette.
- **Troubleshooting-Eintrag "Keine Logs: No journal files were found" in
`docs/INSTALLATION.md`.** Auf einem der Testcontainer war
`systemd-journald.service` kaputt (`failed`, `status=243/CREDENTIALS`),
`journalctl` lieferte `No journal files were found.` Da der Dienst seit
v0.4.1 **ausschliesslich** nach journald loggt (kein FileHandler, kein
Logverzeichnis — bewusste Entscheidung), gibt es dann gar keine Dienstlogs:
ein blinder Fleck, der vor jeder Fehlersuche per
`systemctl status systemd-journald` auszuschliessen ist. Als Notbehelf ist
der Vordergrund-Aufruf dokumentiert
(`cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m
pdf_ocr_hotfolder --config ...`, das `cd` ist zwingend, s. 0.6.2).
Ausdruecklich festgehalten: das ist **kein** generelles LXC-Muster — der
zweite Testcontainer war in Ordnung, es war Schaden auf genau dieser
Maschine.
### Changed
- `AI_AGENT_BRIEFING.md`: der Kommentar zu `requirements.txt` in der
Projektstruktur nannte noch "ocrmypdf 16.x!" — genau die Version, die seit
0.6.1 als unbrauchbar gilt und gegen die der Pin schuetzt. Korrigiert auf
17.x.
## [0.6.2] - 2026-09-22
### Fixed
- Der `--check-config`-Befehl, den `update.sh` in der Zusammenfassung ausgibt,
lief so wie gedruckt nicht (`No module named pdf_ocr_hotfolder`). Das Paket
wird nicht pip-installiert, sondern nach `/opt/pdf-ocr-hotfolder` kopiert und
nur ueber das Arbeitsverzeichnis gefunden — dem Hinweis fehlte das
vorangestellte `cd`. Betraf auch die Beispiele in README.md,
docs/INSTALLATION.md, docs/UPDATE.md und docs/OS-UPGRADE.md.
Gefunden beim Update-Test v0.3.1 -> v0.6.1 auf Debian 12.
## [0.6.1] - 2026-09-22
> **Fuer Bestandsinstallationen wichtig.** Wer 0.6.0 bereits eingespielt hat,
> laeuft auf Debian 12 mit hoher Wahrscheinlichkeit im Totalausfall: der Dienst
> meldet `active`, `--check-config` meldet "Preflight ok" — und **jede** PDF
> landet in `error/`. Nach dem Update auf 0.6.1 nachsehen, ob in `error/`
> unverarbeitete Dateien liegen, und diese zurueck nach `incoming/` schieben.
> Der Updater faehrt jetzt selbst einen Rauchtest, der so einen Zustand sofort
> aufdeckt.
### Fixed
- **ocrmypdf-Pin von 16.13.0 auf 17.4.1 korrigiert — das war ein stiller
Totalausfall.** 0.6.0 pinnte `ocrmypdf==16.13.0`. Auf Bestandssystemen mit
vorher `ocrmypdf>=16.0` war das ein **Downgrade** von 17.4.1, und auf
Debian 12 (Ghostscript 10.0.0) bricht ocrmypdf 16.13.0 bei jeder PDF ab:
```
MissingDependencyError: Ghostscript 10.0.0 through 10.02.0 (your version:
10.0.0) contain serious regressions that corrupt PDFs with existing text
```
Ursache, verifiziert im Quelltext von
`ocrmypdf/builtin_plugins/ghostscript.py::check_options()`: bis
einschliesslich 16.x laeuft die Ghostscript-Pruefung **bedingungslos** —
`skip_text=true` allein genuegt, `output_type` wird gar nicht geprueft,
obwohl die Fehlermeldung selbst `--output-type pdf` empfiehlt. Ab **17.0.0**
umschliesst denselben Block ein
`if options.output_type.startswith('pdfa'):`; ohne PDF/A wird Ghostscript
nicht angefasst. `run_ocr()` setzt bei leerem `pdfa_level` genau
`output_type="pdf"` und hielt sich damit faelschlich fuer sicher.
Betroffen war nicht nur das Update, sondern ebenso jede **Neuinstallation**:
`skip_text = true` ist der Default. — 17.4.1 ist die auf Debian 12 + gs 10.0.0
real verifizierte Version; `skip_text` bleibt in 17.x als Alias fuer
`mode='skip'` unterstuetzt, `run_ocr()` musste nicht angepasst werden.
- **Preflight prueft jetzt die reale Bedingung.** `check_preflight()` sah die
Ghostscript-Version bisher nur bei gesetztem `pdfa_level` an (`if pdfa_level:`)
— genau deshalb ging der kaputte Zustand als "Preflight ok" durch. Die neue
Bedingung (`_gs_block_reason()`) bildet ocrmypdf nach:
```
betroffene GS-Version UND skip_text UND (pdfa_level ODER ocrmypdf < 17)
```
Die Signatur ist jetzt `check_preflight(pdfa_level, skip_text)`; alle
Aufrufstellen (`run()`, `run_once()`, `--check-config`) reichen beides durch.
`redo_ocr` steht bewusst **nicht** in der Bedingung: die Config kennt keinen
solchen Key, und ein erfundener waere schlimmer als ein fehlender.
Ergebnis: der Dienst bricht beim **Start** mit Exit 2 ab statt bei der ersten
Datei, und `--check-config` meldet den Zustand als **Fehler** (Exit 2) — also
auch mitten im Update. Die Meldung nennt beide Auswege: Ghostscript >= 10.02.1
aus bookworm-backports (der Installer bietet das an) oder
`[ocr].skip_text = false`.
### Added
- **`update.sh` macht Versionsspruenge der Kernabhaengigkeiten sichtbar.** Die
Versionen der in `requirements.txt` gepinnten Pakete werden vor und nach
`pip install` gemessen; Downgrades erscheinen als `[WARN]`, Upgrades und neue
Pakete als `[INFO]`, beides zusaetzlich in der Abschluss-Zusammenfassung. Das
Downgrade 17.4.1 -> 16.13.0 verschwand bisher wortlos hinter
`[INFO] Dependencies ok ✓`.
- **Rauchtest in `update.sh`.** Nach dem Start jeder Instanz geht eine winzige
Test-PDF durch die **echte** Pipeline; der Test gilt als bestanden, wenn sie
in `outgoing/` ankommt. Erst das deckt einen Totalausfall auf, den systemd
nicht sieht.
- Die Test-PDF (694 Bytes, eine Seite) steckt als base64 im Skript — kein
Pillow, kein `gs`, kein `convert` noetig.
- Eindeutiger Dateiname (`__smoketest_update_<zeitstempel>_<pid>.pdf`), der
mit keiner Kundendatei kollidieren kann.
- **Raeumt restlos auf** — Testdatei und Ergebnis, in `incoming/`, `working/`
(inkl. `__ocr_`-Zwischendatei), `outgoing/`, `error/` und Archiv, auch bei
Fehlschlag und Timeout.
- **Uebersprungen** bei Instanzen mit aktivem `[upload.nextcloud]`,
`[upload.sftp]`, `[notify.email]` oder `[upload.folder]` mit gesetztem
`target`: dort wuerde die Testdatei nach aussen gehen, im Zweifel zum
Kunden. `[upload.folder]` ohne `target` schreibt nach `outgoing/` und ist
harmlos.
- Wartezeit `SMOKE_TIMEOUT` (Standard 90 s), danach durchgefallen — das
Skript haengt nicht.
- Ein Fehlschlag setzt den Exit-Code auf 1 und nennt den `journalctl`-Befehl,
rollt aber **nichts** zurueck.
- Abschaltbar mit `--no-smoke-test`, dokumentiert in `--help`.
- `--check-config` zeigt zusaetzlich `skip_text` sowie die installierte
ocrmypdf- und Ghostscript-Version an.
### Changed
- **Doku praezisiert.** `config.example.toml`, `config.py`,
`docs/INSTALLATION.md` und `docs/UPDATE.md` behaupteten sinngemaess,
`pdfa_level = ""` sei der sichere Default gegen den Ghostscript-Bug. Das
stimmt so nicht: die Entwarnung haengt an der ocrmypdf-Version und gilt erst
ab 17. Der Ghostscript-Abschnitt in `INSTALLATION.md` stellt die Bedingung
jetzt je ocrmypdf-Major gegenueber und nennt `skip_text = false` als zweiten
Weg; `UPDATE.md` beschreibt Rauchtest und Versionssprung-Meldung.
- Kommentarblock in `requirements.txt` korrigiert: der Schutz gilt gegen den
naechsten ungewollten Major-Sprung (18), **nicht** gegen 17 — samt
Begruendung, warum 16.x fuer uns unbrauchbar ist.
### Tests
- 152 statt 135 Tests. Neu: die Preflight-Matrix aus GS-Version x `skip_text` x
`pdfa_level` x ocrmypdf-Major — darunter der Fall, der durchrutschte
(betroffene GS-Version + `skip_text=true` + leeres `pdfa_level` +
ocrmypdf 16.x muss `PreflightError` ausloesen) und die Gegenprobe, dass
dieselbe Config mit ocrmypdf 17.x **nicht** ausloest (sonst startet keine
Debian-12-Bestandsinstanz mehr). Dazu `ocrmypdf_checks_gs_always()`, der
Abbruch in `run_once()` und Exit 2 bei `--check-config`.
## [0.6.0] - 2026-09-22
### Added
- **Wiederaufnahme aus `working/` beim Start.** `process_pdf()` verschiebt das
Original vor dem OCR nach `working/`. Wurde der Dienst dort hart abgeschossen
(SIGKILL nach `TimeoutStopSec`), blieb die Datei liegen und wurde **nie wieder
angefasst** — stiller Datenverlust. `_scan_working()` greift sie jetzt beim
Start auf (vor `incoming/`), das OCR laeuft fuer sie neu. Liegt in `incoming/`
eine gleichnamige, andere Datei, bekommt die wiederaufgenommene einen
Zeitstempel angehaengt, damit sich beide nicht ueberschreiben. Liegt in
`working/` bereits eine andere Datei desselben Namens, bricht `process_pdf()`
fuer die neue ab und laesst sie in `incoming/` liegen, statt den laufenden
Vorgang stillschweigend zu ueberschreiben.
- **Unvollstaendige OCR-Fragmente werden geloescht.** Die Zwischendatei, in die
ocrmypdf schreibt, traegt jetzt das Praefix `__ocr_` (`OCR_TEMP_PREFIX`).
Bleibt so eine Datei nach einem harten Stopp in `working/` liegen, ist sie als
Eingabe unbrauchbar und als Ergebnis wertlos — sie wird beim Start mit einer
Warnung entfernt, damit sie niemand fuer ein fertiges PDF haelt.
- **`--check-config`**: prueft eine Instanz-Config, ohne irgendetwas zu
verarbeiten (hat Vorrang vor `--once`). Zeigt die vier Pfade inkl. Hinweis auf
noch fehlende Verzeichnisse, Sprachen, Seiten-Timeout und PDF/A-Level, faehrt
Preflight und `[output]`-Validierung und gibt alle Warnungen aus.
Exit **0** = sauber, **1** = nur Warnungen, **2** = Fehler (Dienst wuerde nicht
starten).
- **Legacy-Warnungen fuer `[ocr].timeout` und `[ocr].pdfa_level`.** Ein
`timeout >= 900` stammt fast sicher aus einer Config vor 0.4.0, wo der Wert ein
wirkungsloses Gesamt-Timeout mit Default 1800 war — seither sind es Sekunden
**pro Seite** (Richtwert 300). Ein gesetztes `pdfa_level` weist auf den
Ghostscript-Bug hin. Die Texte stehen nur in `config.py`
(`legacy_warnings()`), weil sie sowohl beim Dienststart ins Log gehen als auch
von `--check-config` ausgegeben werden.
- **Unbekannte Config-Keys werden gemeldet** statt still verworfen.
`_collect_unknown_keys()` sammelt Tippfehler (`[ocr].langauges`), Optionen aus
aelteren Versionen, unbekannte Sektionen und unbekannte Upload-/Notify-Targets
in `Config.unknown_keys`; die Meldung nennt den vollen Pfad. Warnung, kein
Fehler — der Dienst startet, der Eintrag tut nur nichts.
- **`TimeoutStopSec=300` in der Template-Unit**: ein laufendes OCR darf beim
Stoppen zu Ende laufen. Ein `systemctl stop` kann dadurch pro Instanz bis zu
5 Minuten dauern — das ist gewollt, ein SIGKILL wuerde den Durchlauf kosten.
- **Feste Pins in `requirements.txt`** (`ocrmypdf==16.13.0`, `watchdog==6.0.0`,
`requests==2.33.1`, `paramiko==4.0.0`). Ohne Pins zieht ein
`pip install --upgrade` beim Update ungefragt einen Major-Sprung ein; ocrmypdf
16 -> 17 wuerde alle Instanzen auf einmal reissen. Geprueft gegen Python 3.11
(Debian 12) und 3.13 (Debian 13), Wheels fuer beide vorhanden.
- 40 neue Tests (Wiederaufnahme aus `working/`, `--check-config`,
Config-Warnungen). Suite jetzt **135 Tests**.
### Changed
- **Die Betriebsdoku ist in drei Dokumente aufgeteilt.** Der README ist wieder
der Einstieg (Kurzbeschreibung, Features, Schnellstart, Verzeichnis-Layout,
Config-Ueberblick) und verlinkt:
- `docs/INSTALLATION.md` — Erstinstallation, Basis-Install vs. Instanz-Anlage,
die Abfragen pro Instanz, Multi-Instanz-Betrieb, LXC (Error 226/NAMESPACE),
Ghostscript auf Debian 12, Instanz manuell loeschen und die vollstaendige
**Konfigurationsreferenz**.
- `docs/UPDATE.md` — Ablauf von `update.sh`, was es nicht anfasst,
Backup-Inhalt/-Rechte/-Rotation, Rollback und dessen Grenzen,
`--check-config` mit den Exit-Codes und Config-Drift.
- `docs/OS-UPGRADE.md` — Debian-Major-Upgrade als eigener Ablauf.
`AI_AGENT_BRIEFING.md` bleibt der Agent-Kontext (Aufbau und Begruendungen) und
verweist fuer Ablaeufe auf die drei Dokumente, statt sie zu wiederholen.
Nichts wird doppelt gepflegt.
- **`update.sh` komplett ueberarbeitet** (`--help`, `--rebuild-venv`,
`set -Eeuo pipefail`):
- **venv-Health-Check und Neubau.** Geprueft werden Existenz, Lauffaehigkeit
des Interpreters, `major.minor` gegen das System-Python und `pyvenv.cfg`.
Passt etwas nicht — typisch nach einem Debian-Major-Upgrade, systemd meldet
dann `203/EXEC` —, wird die venv neu gebaut, auch ohne `--rebuild-venv`. Der
Neubau ist ganz oder gar nicht: alte venv weg sichern, neu bauen,
Requirements installieren, **erst bei Erfolg** die alte loeschen; scheitert
etwas, wird zurueckgerollt und hart abgebrochen. Scheitert pip an einem Pin,
nennt das Skript das gescheiterte Paket und den naechsten Schritt
("requirements.txt anheben").
- **apt-Sync auch beim Update.** Die Paketliste wird aus `install.sh`
extrahiert (einzige Quelle, Marken `BEGIN/END apt-packages`) und
installiert; nachinstallierte Tesseract-Sprachpakete bleiben unangetastet
(kein purge, kein autoremove). Fehlschlaege warnen nur.
- **Haertere Verifikation.** Nach dem Start prueft `verify_unit()` nicht nur
`is-active`, sondern auch `is-failed` und den Restart-Zaehler — ein
Crash-Loop galt bei `Type=simple` bisher als Erfolg. Die Zusammenfassung
stellt Soll gegen Ist und meldet eine **Regression** namentlich.
- **Vollstaendiges Backup.** Gesichert werden Code, alle Instanz-Configs, die
Template-Unit, alle Drop-ins und ein `pip-freeze.txt` der alten venv — ohne
venv und ohne Datenverzeichnisse. Weil die Configs Klartext-Passwoerter
enthalten, wird das Archiv mit `umask 077` erzeugt und auf `0600 root:root`
gesetzt, das Verzeichnis auf `700`. Rotation: die letzten 5 Archive bleiben.
- **ERR-Trap.** Bricht das Update ab (Fehler, Strg-C, `kill`), sagt das Skript,
ob auf der Platte schon getauscht wurde, startet die vorher laufenden
Instanzen wieder und nennt Backup-Datei und Rollback-Befehl.
- **Instanz-Erfassung** deckt jetzt auch `activating` und `failed` ab (ueber
`list-units --all`, `list-unit-files` und die vorhandenen Configs). Vorher
kaputte Instanzen werden mitgestartet, gelten aber erst als Erfolg, wenn sie
danach wirklich laufen; bewusst gestoppte bleiben gestoppt.
- **Config-Pruefung vor dem Start**: `--check-config` je Instanz, Exit 2 zaehlt
als Fehler (Update-Exit 1), Exit 1 wird als Warnung samt Nachstell-Befehl
ausgegeben. Kennt der installierte Code das Flag noch nicht, wird die
Pruefung uebersprungen und das Update laeuft weiter.
- **`install.sh` repariert eine kaputte venv.** Bisher reichte das blosse
Vorhandensein von `venv/`, um den Basis-Install zu ueberspringen — nach einem
Distributions-Upgrade hat der Installer damit gar nichts repariert. Jetzt wird
die venv gegen das System-Python geprueft und bei Drift nach
`venv.old-<timestamp>` gesichert und neu gebaut.
- Die apt-Paketliste steht als **einzige Quelle** in `install.sh` in der Funktion
`pdf_ocr_apt_packages()` zwischen den Marken `# --- BEGIN apt-packages` /
`# --- END apt-packages`. `update.sh` schneidet den Block heraus und wertet ihn
aus — Marken und Funktionsname duerfen sich nicht ohne Anpassung aendern.
### Fixed
- **Dateien in `working/` gingen nach einem harten Stopp still verloren.** Siehe
Wiederaufnahme oben — der Fall trat bei jedem SIGKILL waehrend eines OCR-Laufs
auf, also auch bei einem Update ohne `TimeoutStopSec`.
- **Tippfehler in Config-Keys fielen nicht auf.** `load_config()` filterte
stumm gegen die Dataclass-Annotationen; `[ocr].langauges` lief damit
wirkungslos mit. Jetzt gibt es eine Warnung mit vollem Key-Pfad.
## [0.5.0] - 2026-09-22
### Added
- Der Installer weist einen Archiv-Pfad ab, der auf `incoming/`, `outgoing/`,
`working/` oder `error/` der Instanz zeigt — im Eingang wuerde das Original
sonst endlos neu aufgegriffen.
- **`install.sh` fragt beim Anlegen einer Instanz die OCR-Sprachen ab**
(`Tesseract-Sprachen [deu+eng]:`). Die Wahl gilt bewusst **pro Instanz** —
ein Hotfolder `buchhaltung` kann mit `deu` laufen, ein Hotfolder `export` mit
`deu+eng+fra`. Der Installer weist vorher darauf hin, dass jede zusaetzliche
Sprache Laufzeit **und** Erkennungsqualitaet kostet, die Liste also eng
gehalten werden sollte. Das Eingabeformat wird geprueft (Sprachcodes mit `+`
verbunden, `chi_sim` & Co. erlaubt); bei Unsinn wird erneut gefragt statt
abzubrechen.
- **Sprachpakete werden nachinstalliert.** Jeder eingegebene Code wird gegen
`tesseract --list-langs` geprueft. Fehlt eine Sprachdatei, bietet der
Installer das passende apt-Paket an (`tesseract-ocr-<code>`, Unterstrich wird
zum Bindestrich: `chi_sim` → `tesseract-ocr-chi-sim`). Lehnt der User ab oder
laesst sich das Paket nicht installieren, warnt der Installer, dass OCR mit
dieser Sprache **bei jeder Datei** scheitern wuerde, und fragt die Sprachen
erneut ab — so kann die Sprache einfach wieder rausgeworfen werden. Ist
`tesseract` nicht aufrufbar, wird die Pruefung uebersprungen und die Eingabe
unveraendert uebernommen.
- **Abfrage `Original nach erfolgreichem OCR archivieren? [j/N]:`** — Default
nein, also weiterhin `original_on_success = "delete"`. Bei ja wird der
Archiv-Pfad abgefragt (Vorschlag `<basis>/archive`), angelegt und auf den
Service-User gechownt; ein Archiv ausserhalb des Instanz-Basis-Pfads bekommt
ein eigenes `chown -R`.
### Changed
- Die Instanz-Config wird weiterhin per `sed` aus `config.example.toml`
erzeugt, substituiert jetzt aber zusaetzlich `[ocr].languages`,
`[output].original_on_success` und `[output].archive_dir` — bisher waren das
die Beispiel-Defaults, `archive_dir` musste von Hand nachgetragen werden.
Die Ausdruecke sind am Zeilenanfang verankert (`^key[[:space:]]*=`), damit die
deutschen Kommentarzeilen ueber den Keys unangetastet bleiben, und
Pfad-Variablen laufen durch `sed_escape_repl()` (maskiert `\`, `&`, `|`) —
Pfade mit Sonderzeichen landen damit korrekt in der Config.
- Nach dem sed-Lauf liest der Installer die drei Keys aus der erzeugten Config
zurueck und vergleicht sie mit der Eingabe. Erst wenn das passt, nennt die
Abschluss-Zusammenfassung zusaetzlich die gewaehlten **Sprachen** und (bei
Archivierung) das **Archiv-Verzeichnis**; sonst gibt es eine Warnung.
## [0.4.1] - 2026-09-22
### Fixed
- **veraPDF-FAIL hat das Original immer gelöscht.** Schlug die PDF/A-Validierung
fehl, wanderte das OCR-Ergebnis nach `error/` und das Original wurde per
`unlink()` entfernt — unabhängig von `[output].original_on_success`. Wer
`archive` konfiguriert hatte, verlor die Datei also ausgerechnet im
Fehlerfall. Der FAIL-Pfad nutzt jetzt dieselbe `_dispose_original()`-Logik
wie der Erfolgsfall: `archive` legt das Original samt
Timestamp-Kollisionsschutz im `archive_dir` ab, `delete` verhält sich wie
bisher. Die Log-Meldung nennt jetzt beides — wohin das OCR-Ergebnis ging und
was mit dem Original passiert ist.
### Removed
- Das nie benutzte Logverzeichnis `/var/log/pdf-ocr-hotfolder/` wird nicht mehr
vom Installer angelegt und ist aus README und Briefing entfernt. Es hat nie
ein Logfile enthalten: `_setup_logging()` nutzt `logging.basicConfig()` ohne
FileHandler, der Dienst loggt nach stdout → journald. **journald ist damit die
einzige Log-Quelle** (`journalctl -u pdf-ocr-hotfolder@<instanz> -f`).
Weder Installer noch Updater fassen das Verzeichnis an: ein vorhandenes,
leeres `/var/log/pdf-ocr-hotfolder/` kann auf bestehenden Installationen
gefahrlos von Hand entfernt werden (`sudo rmdir /var/log/pdf-ocr-hotfolder`).
### Added
- 3 neue Tests für den veraPDF-FAIL-Pfad (`delete`, `archive`,
Archiv-Namenskollision); veraPDF wird dabei gemockt. Suite jetzt 95 Tests.
## [0.4.0] - 2026-09-22
### Added
- `[ocr].timeout` ist jetzt wirksam: der Wert wird als `tesseract_timeout`
(Sekunden pro Seite) an ocrmypdf durchgereicht. Bisher war der Key zwar
dokumentiert, wurde aber nirgends gelesen.
- `check_output_config()` validiert zusätzlich `[output].name_mode`. Ein Tippfehler
führt jetzt beim Start zum Abbruch mit Exit-Code 2, statt erst pro Datei
zuzuschlagen — und zwar bisher **nach** dem Verschieben nach `working/`,
wo die Datei dann liegen blieb.
- Neue Exception `ConfigError` in `pdf_ocr_hotfolder.config` — fehlende
`[paths]`-Sektion oder ein fehlender Pfad-Eintrag liefern eine deutsche
Fehlermeldung mit Datei- und Key-Nennung statt eines nackten `KeyError`-Tracebacks.
Die CLI bricht damit sauber mit Exit-Code 2 ab.
- `pytest.ini` mit `testpaths = tests`, damit `pytest` aus dem Repo-Root läuft.
- 35 neue Tests: Fehlerzählung (Exception, Upload, Stabilitäts-Timeout),
Config-Fehlermeldungen, `tesseract_timeout`-Durchreichung (ocrmypdf gemockt)
und `upload_folder()`.
### Changed
- **`[ocr].timeout` hat eine neue Bedeutung — für bestehende Installationen relevant!**
Der Wert ist kein (nie implementiertes) Gesamt-Timeout pro PDF mehr, sondern
das Limit **pro Seite** für Tesseract. Der Default sinkt entsprechend von
`1800` auf `300`. Wer den alten Wert `1800` in seiner `config.toml` stehen hat,
gibt Tesseract damit 30 Minuten **je Seite** — bitte auf einen Seiten-Wert
anpassen (Richtwert 300).
`0` bedeutet "kein eigenes Limit": der Wert wird dann gar nicht erst
durchgereicht, weil ocrmypdf `tesseract_timeout=0` als "OCR komplett
überspringen" interpretiert.
- `_dispatch_uploads()` liefert jetzt die Namen der fehlgeschlagenen Upload-Ziele
zurück; die doppelte `enabled`-Prüfung (Service + Uploader) ist entfallen —
die Uploader prüfen das selbst.
- `upload_folder()` kopiert mit `shutil.copyfile()` statt
`read_bytes()`/`write_bytes()` — große PDFs landen nicht mehr komplett im
Speicher. Die Selbst-Ziel-Erkennung bleibt unverändert.
### Fixed
- `OcrConfig.pdfa_level` hatte im Code noch den Default `"2"`, obwohl
`config.example.toml` seit 0.2.2 bewusst `""` setzt (Ghostscript-Bug, Issue #3).
Eine Config ohne `[ocr]`-Sektion bzw. ohne den Key lief damit ungewollt in
PDF/A. Default im Code jetzt ebenfalls `""`.
- Eine Exception **nach** dem OCR (z.B. ein fehlgeschlagener
`shutil.move()` nach `outgoing/`) wurde nur im Worker-Callback geloggt.
`error_count` blieb 0 und `--once` lieferte trotz Fehlschlag Exit-Code 0.
Jede Exception aus `process_pdf()` zählt jetzt als Fehler, wird geloggt,
löst eine Fehler-Mail aus und die Datei wandert — soweit noch auffindbar
(`incoming/` oder `working/`) — nach `error/`.
- Fehlgeschlagene Uploads waren folgenlos: die Rückgabewerte der Uploader wurden
verworfen, es ging sogar eine Erfolgs-Mail raus. Jetzt zählt mindestens ein
fehlgeschlagenes Ziel als Fehler und die E-Mail geht als **FEHLER** raus, mit
Nennung der betroffenen Ziele. Das OCR-PDF bleibt bewusst in `outgoing/`
liegen (das OCR selbst war ja erfolgreich) — das steht so auch im Log.
- Lief der Stabilitäts-Check einer Datei in den 60-Sekunden-Timeout, gab es nur
ein `log.warning`; `--once` meldete Exit-Code 0. Jetzt `log.error` +
`error_count`. Die Datei bleibt bewusst in `incoming/` liegen und wird beim
nächsten Lauf erneut versucht. Eine zwischenzeitlich *verschwundene* Datei
wird davon unterschieden und zählt weiterhin nicht als Fehler.
## [0.3.1] - 2026-04-10
### Fixed
- **Issue #4**: LXC/Container-Kompatibilität — systemd-Hardening (`PrivateTmp`, `ProtectSystem`, etc.)
verursacht Error 226/NAMESPACE in LXC-Containern. Installer erkennt Container-Umgebung automatisch
und bietet ein Drop-in an. Zusätzlich liegt `systemd/lxc-compat.conf` als Vorlage im Repo.
- **Issue #5**: `WorkingDirectory=/opt/pdf-ocr-hotfolder` in der systemd Template-Unit ergänzt —
ohne diesen Eintrag konnte das Python-Modul nicht gefunden werden.
- **Issue #6**: Auf Debian 12 bietet der Installer bei betroffenen Ghostscript-Versionen (10.0.0–10.02.0)
jetzt automatisch an, bookworm-backports zu aktivieren und GS zu upgraden (statt nur zu warnen).
## [0.3.0] - 2026-04-09
### Added
- Neue Config-Sektion `[output]` mit:
- `name_mode` — Platzierung des Tags im Dateinamen: `"prefix"`, `"suffix"` (vor Extension), `"none"`
- `name_tag` — verbatim einzufügender String, z.B. `"OCR_"` oder `"_OCR"`
- `original_on_success` — `"delete"` (alter Default) oder `"archive"`
- `archive_dir` — Zielverzeichnis für `"archive"`, mit Kollisions-Schutz (Timestamp-Suffix)
- Runtime-Validierung der Output-Config in `check_output_config()`
- 20 neue Tests für `build_output_name()`, `check_output_config()` und `process_pdf()`
mit allen Kombinationen aus Modus + Original-Behandlung
### Changed
- `process_pdf()` nimmt jetzt `output_cfg: OutputConfig` als Pflicht-Argument
## [0.2.2] - 2026-04-09
### Fixed
- **Issue #3**: Ghostscript 10.0.0–10.02.0 (Debian 12 default) zerschießen OCR mit PDF/A + `skip_text=true`.
- `config.example.toml`: `pdfa_level = ""` als sicherer Default
- Runtime-Preflight: Prüft `gs --version` wenn `pdfa_level` gesetzt ist, bricht mit klarer Fehlermeldung ab
- `install.sh`: warnt bei betroffenen GS-Versionen mit Upgrade-Hinweis auf bookworm-backports
### Added
- `is_ghostscript_broken()` / `detect_ghostscript_version()` in `pdf_ocr_hotfolder.service`
- 19 weitere pytest-Tests für GS-Versions-Detection (parametrisiert) und Preflight-Kombinationen
## [0.2.1] - 2026-04-09
### Fixed
- **Issue #1**: Preflight-Check beim Start prüft jetzt `tesseract` und `gs` (Ghostscript). Fehlt eine Abhängigkeit, beendet sich der Service sofort mit Exit-Code 2 und klarer Fehlermeldung statt erst bei der ersten Datei.
- **Issue #2**: `--once`-Modus liefert jetzt Exit-Code `1`, sobald **mindestens ein** PDF fehlgeschlagen ist. Exit-Code `0` nur bei vollständigem Erfolg (inkl. "keine Dateien vorhanden"). Exit-Code `2` bei Preflight-Fehler.
### Added
- Public API: `HotfolderService.run_once()`, `.success_count`, `.error_count`, `.ensure_dirs()`
- `check_preflight()` / `PreflightError` in `pdf_ocr_hotfolder.service`
- pytest-Test-Suite (`tests/`) mit 11 Tests — deckt alle Szenarien aus Issue #1 und #2 ab
- `ocrmypdf`-Import in `processor.py` ist jetzt lazy (Tests ohne ocrmypdf-Installation möglich)
## [0.2.0] - 2026-04-08
### Added
- **Multi-Instanz-Support** via systemd Template-Unit `pdf-ocr-hotfolder@<name>.service`
- Pro Instanz: eigene Config (`/etc/pdf-ocr-hotfolder/<name>.toml`), eigene Datenverzeichnisse (`/var/lib/pdf-ocr-hotfolder/<name>/…`), optional eigener Service-User via Drop-in
- **Instanz-Manager in `install.sh`**: erkennt bestehende Instanzen bei Re-Run, fragt nach weiteren, listet Namen + Status
- `update.sh` stoppt/startet automatisch **alle** laufenden Instanzen
### Changed
- Single-Unit `pdf-ocr-hotfolder.service` durch Template-Unit `pdf-ocr-hotfolder@.service` ersetzt
- Installer fragt nicht mehr einmalig nach Service-User, sondern **pro Instanz**
### Removed
- Alte Single-Config unter `/etc/pdf-ocr-hotfolder/config.toml` — wird nicht mehr erzeugt
## [0.1.0] - 2026-04-08
### Added
- Initiale Version (Komplettes Rewrite des alten Bash-Tools `pdf-tool`)
- Python-Implementation auf Basis von `ocrmypdf` (Library, kein Subprozess)
- Hotfolder-Watcher mit `watchdog` (created/moved/closed Events)
- File-Stability-Check (wartet bis Scanner fertig geschrieben hat)
- ThreadPool für parallele PDF-Verarbeitung (`max_workers`)
- Upload-Targets: lokaler Ordner, Nextcloud (WebDAV via `requests`), SFTP (`paramiko`)
- E-Mail-Notify (`smtplib`, immer / nur Fehler / nie)
- Optional veraPDF-Validierung
- TOML-Konfiguration (`tomllib` aus stdlib, Python ≥3.11)
- systemd-Unit mit Hardening-Optionen
- `install.sh` mit interaktivem Service-User-Prompt
(lokal anlegen oder bestehenden lokalen/AD-User übernehmen)
- `update.sh` mit Backup, Code-Sync und Service-Reload
- README.md, AI_AGENT_BRIEFING.md