Files
pdf-ocr-hotfolder/CHANGELOG.md
T
techadmin 1b67c846a2 fix: Updater meldet den LXC-Drop-in nur noch, wenn sich wirklich etwas aendert (v0.7.2)
Nachlese aus der Verifikation von v0.7.1 auf einem frischen Debian 12.

- update.sh meldete "LXC-Drop-in nachgezogen ✓" auch bei unveraenderter
  Datei — dieselbe Klasse Unwahrheit wie das Ghostscript-Haekchen, das in
  v0.7.1 behoben wurde. Jetzt wird verglichen und ehrlich gemeldet.
- "Quelle wieder entfernt" ist Teil der schlechten Nachricht und kommt als
  WARN statt INFO.

254 Tests gruen.

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

44 KiB
Raw Permalink Blame History

Changelog

[0.7.2] - 2026-09-23

Nachlese aus der Verifikation von 0.7.1 auf einem frischen Debian-12-Container.

Fixed

  • update.sh meldete "LXC-Drop-in nachgezogen ✓" auch dann, wenn die Datei bereits identisch war — dieselbe Sorte Unwahrheit wie das Ghostscript-Haekchen in 0.7.0. Es wird jetzt verglichen und nur gemeldet, was tatsaechlich passiert ist ("bereits aktuell" vs. "nachgezogen").
  • Die Zeile "Quelle wieder entfernt" gehoert zur schlechten Nachricht und kommt jetzt als WARN statt als INFO.

[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