rewrite eines Scanprograms in sveltekit
  • Svelte 57.9%
  • TypeScript 20.3%
  • Python 20%
  • HTML 0.9%
  • Shell 0.8%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-26 10:09:01 +02:00
.github .github/pull_request_template.md hinzugefügt 2026-04-15 15:36:24 +00:00
apps/web auswahl-fix 2026-08-26 07:14:00 +02:00
scripts buildscript 2026-07-27 08:03:11 +02:00
services export pfad patch 2026-08-26 10:09:01 +02:00
.env.example pdf-worker aus node_modules statt static/ — driftsicher 2026-07-27 08:31:14 +02:00
.gitignore ki-bereich-reste entfernt, pdf.worker auf 6.1.200, ollama-limits, gitignore 2026-07-24 08:16:13 +02:00
CLA.md Init CLA 2026-04-15 15:29:37 +00:00
docker-compose.yml pdf-worker aus node_modules statt static/ — driftsicher 2026-07-27 08:31:14 +02:00
license.md Init license 2026-04-15 15:26:38 +00:00
migrate_mail_trash.sql mail-trash 2026-06-03 09:09:21 +02:00
README.md readme aktualisierungen 2026-07-27 08:10:37 +02:00
RELEASE.md buildscript 2026-07-27 08:03:11 +02:00

digiscan-ng

Professionelles Dokumenten-Scanning- und Indexierungssystem mit Blind-Double-Entry-Verfahren, OCR, KI-Ersterfassung und Mail-Eingang.

Scanner-PC (Linux)              Server (.77)
scanimage (SANE)                SvelteKit + PostgreSQL
ocrmypdf (lokal)                Python Worker (OCR, Export)
PDF upload ───────────────────► Indexierung (2× blind)
                                Konflikt-Auflösung (Merger)
                                Export: PDF + CSV
                                Mail-Fetcher (IMAP → PDF)

Stack

Komponente Technologie
Frontend + API SvelteKit 2 / Svelte 5 (Node-Adapter, Port 3000)
OCR-Worker Python FastAPI + ocrmypdf + pikepdf
Datenbank PostgreSQL 17
Mail-Fetcher Python + imaplib, eigener Docker-Service
Scanner-Agent Python systemd-Service auf Scanner-PC (Port 8089)
KI Ollama (qwen3:1.7b), lokal
Reverse Proxy Caddy (HTTPS + HTTP/2, internes Zertifikat)

Benutzerrollen

Rolle Kann
indexer Dokumente blind erfassen (1. und 2. Durchlauf)
merger Konflikte auflösen — sieht beide Erfassungen
admin Alles: Benutzer, Dokumenttypen, Export, Mail-Eingang, KI

Standard-Login nach Erstinstallation: admin / Wert aus ADMIN_PASSWORD (.env) — sofort in der UI ändern!


Installation & Deployment

Es gibt zwei Betriebsarten, die klar getrennt sind:

  • Entwicklungssystem — hier wird der Code gebaut und getestet. Braucht das Git-Repo, Docker und die Build-Werkzeuge. Baut die Container-Images und schiebt sie in die Registry.
  • Produktionssystem — läuft im Betrieb. Braucht kein Repo und keine Build-Werkzeuge, nur Docker und Zugriff auf die Registry. Holt fertige Images mit docker compose pull.

Die fertigen Images liegen in der Forgejo-Container-Registry unter link.piriot.de/martin/:

Dienst Image
web link.piriot.de/martin/digiscan-web
worker link.piriot.de/martin/digiscan-worker
mail-fetcher link.piriot.de/martin/digiscan-mail-fetcher

Postgres und Ollama kommen von Docker Hub und sind auf feste Versionen gepinnt. Welche Image-Version läuft, steuert VERSION in der .env (z.B. VERSION=3.1.0).


A) Entwicklungssystem aus dem Git-Repo aufsetzen

git clone https://link.piriot.de/martin/digiscan-ng.git
cd digiscan-ng

cp .env.example .env
# .env anpassen — Pflichtfelder:
#   POSTGRES_PASSWORD   sicheres Passwort
#   AGENT_SECRET        32+ Zeichen, geheim
#   ORIGIN              https://deine-interne-domain.dom
#   ADMIN_PASSWORD      Erstpasswort
# VERSION bleibt auf "dev" — es wird lokal gebaut, nicht aus der Registry gezogen.

docker compose up -d --build

--build baut die Images lokal aus dem Quellcode. In Netzen mit TLS-aufbrechendem Firmen-Proxy muss vorher BUILD_HTTP_PROXY in der .env gesetzt sein, sonst scheitern apk/apt/pip/npm beim Bauen.

Einen Release bauen und in die Registry schieben: siehe RELEASE.md.


B) Produktionssystem aufsetzen (nur Images, kein Repo)

Auf dem Entwicklungssystem einmal das Deployment-Paket erzeugen — es enthält docker-compose.yml, .env.example, schema.sql, postgresql.conf und backup.sh, also alles, was per Bind Mount gebraucht wird und nicht im Image steckt:

./scripts/build-deploy-package.sh 3.1.0
# → digiscan-deploy-3.1.0.tar.gz

Dieses Archiv auf das Zielsystem kopieren, dort:

tar xzf digiscan-deploy-3.1.0.tar.gz
cd digiscan-deploy-3.1.0

docker login link.piriot.de        # einmalig pro System
cp .env.example .env               # VERSION ist bereits vorbelegt
# .env ausfüllen (Passwörter, ORIGIN, ...)

mkdir -p data/postgres data/incoming data/export data/backups data/ollama

docker compose pull
docker compose up -d

Beim ersten Start legt Postgres das Schema aus schema.sql an.


Ein Produktionssystem aktualisieren

# VERSION in der .env auf die neue Nummer setzen, dann:
docker compose pull
docker compose up -d

Zurückrollen: alte Nummer in die .env, erneut pull + up -d. Die alten Images bleiben in der Registry.

Achtung bei Schema-Änderungen: schema.sql läuft nur beim allerersten Start gegen eine leere Datenbank. Das Entwicklungsmuster „Datenverzeichnis löschen und neu anlegen" ist auf Systemen mit echten Daten ausgeschlossen. Bringt ein Release Schema-Änderungen mit, müssen diese gesondert eingespielt werden — Details in den jeweiligen Release-Hinweisen.


Reverse Proxy & Zertifikat

Caddy startet automatisch und übernimmt HTTPS. Beim ersten Start wird ein internes Root-Zertifikat generiert — einmalig auf jedem Client importieren (Download unter https://deine-domain/digiscan-ca.crt).


Scanner-Agent (Linux-Scanner-PC)

cd digiscan-ng-agent
pip install -r requirements.txt

# Systemabhängigkeiten:
apt install sane-utils ocrmypdf tesseract-ocr tesseract-ocr-deu

# Als systemd-Service einrichten:
cp digiscan-agent.service /etc/systemd/system/
# SERVICE_URL und AGENT_SECRET in der Unit-Datei anpassen
systemctl enable --now digiscan-agent

Panasonic KV-S5076H: udev-Regel erforderlich für stabilen USB-Pfad:

SUBSYSTEM=="usb", ATTRS{idVendor}=="04da", ATTRS{idProduct}=="0e3f", MODE="0666", SYMLINK+="scanner-panasonic", ATTR{power/autosuspend_delay_ms}="-1"

Umgebungsvariablen

Variable Beschreibung Beispiel
POSTGRES_PASSWORD DB-Passwort sicher123
AGENT_SECRET Shared secret für Scanner-Agent (min. 32 Zeichen) 32-zeichen-geheim
ORIGIN Interne URL der App (siehe Multi-Domain-Setup) https://digiscan.dom
ADMIN_PASSWORD Admin-Passwort beim ersten Start erstpasswort
CHECK_ORIGIN CSRF-Schutz aktivieren (true hinter Reverse Proxy) true
AGENT_PROXY_MODE true wenn HTTPS aktiv (verhindert Mixed-Content) true
MAX_WORKERS Parallele OCR-Prozesse (~75% der CPU-Kerne) 6
OCR_LANG Tesseract Sprachcodes deu+eng
OLLAMA_URL Ollama-Server URL http://ollama:11434
OLLAMA_CONTEXT_RESET_INTERVAL Modell nach N Aufrufen neu laden (0=aus) 25

Multi-Domain-Setup (intern + extern)

digiscan-ng läuft typischerweise intern unter einer .dom-Domain mit selbstsigniertem Caddy-Zertifikat. Soll die App zusätzlich über eine externe .de-Domain erreichbar sein (z.B. für Remote-Zugriff), ist folgendes Setup empfohlen wenn der externe Server ein separater Rechner ist:

Architektur

Internet:443 → nginx (ext. Server, .76) ──HTTP──► Caddy (.77, Port 3001) → SvelteKit
Browser intern                          ──HTTPS──► Caddy (.77, Port 443)  → SvelteKit

Warum ORIGIN auf der internen Domain bleibt

ORIGIN bleibt auf der internen Domain. Externe Nutzer sehen digiscan.de in der Adresszeile (nginx terminiert TLS extern), SvelteKit bekommt den korrekten Host-Header via HOST_HEADER=host und baut URLs entsprechend auf.

Was nicht funktioniert:

  • ORIGIN leer lassen → App startet nicht
  • ORIGIN auf .de setzen → interne Nutzer werden auf externe Domain umgeleitet
  • nginx direkt auf Caddy Port 443 → TLS-Handshake schlägt fehl (self-signed CA)

Caddy — internen HTTP-Port öffnen

In der Caddyfile auf dem Server (.77) einen zusätzlichen unverschlüsselten Port für nginx ergänzen:

:3001 {
    reverse_proxy localhost:3000 {
        header_up Host              {host}
        header_up X-Forwarded-Host  {host}
        header_up X-Forwarded-Proto https
        header_up X-Real-IP         {remote_host}
        header_up X-Forwarded-For   {remote_host}
        flush_interval -1
    }
}
systemctl reload caddy

nginx auf dem externen Server (.76)

Let's Encrypt Zert holen (einmalig):

systemctl stop nginx
certbot certonly --standalone -d digiscan.de
systemctl start nginx

nginx-Konfiguration (/etc/nginx/sites-available/digiscan):

server {
    listen 80;
    server_name digiscan.de;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name digiscan.de;

    ssl_certificate     /etc/letsencrypt/live/digiscan.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/digiscan.de/privkey.pem;

    location / {
        proxy_pass          http://192.168.100.77:3001;  # interne IP + Port 3001
        proxy_set_header    Host              $host;        # wichtig: $host, nicht digiscan.dom
        proxy_set_header    X-Forwarded-Host  $host;
        proxy_set_header    X-Forwarded-Proto https;
        proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_http_version  1.1;
        proxy_set_header    Connection "";
    }
}

.env — zwei Zeilen ergänzen

ORIGIN=https://digiscan.dom     # interne Domain — nicht ändern!
HOST_HEADER=host                # Node-Adapter nutzt Host-Header für URL-Konstruktion
PROTOCOL_HEADER=x-forwarded-proto

Workflow

Scan → Indexierung

  1. Operator startet Scan über Agent-UI (Port 8089 auf Scanner-PC)
  2. Agent scannt ADF, führt OCR durch, lädt PDF hoch
  3. Job erscheint im Dashboard mit Live-Status
  4. Zwei Indexierer erfassen dasselbe Dokument blind (unabhängig voneinander)
  5. System vergleicht: stimmt alles → auto_approved, sonst → conflict
  6. Konflikte gehen in die Merger-Queue (/merge)
  7. Nach Freigabe aller Dokumente: Admin exportiert → PDF + CSV

Dokument-Status-Flow

pending → ocr_done → entry_1_done ──→ auto_approved ──→ exported
                           └──────→ conflict ──→ merged ──┘

Mail-Eingang

  1. Fetcher pollt IMAP-Konten alle 15s, speichert PDF-Anhänge lokal
  2. Admin öffnet Mail-Eingang (/admin/mail-inbox)
  3. Pro Anhang: bestehende oder neue Position auswählen → Import
  4. Wenn alle Anhänge einer Mail verarbeitet: 🗑 Löschen-Button erscheint
  5. Löschen: Mail wandert auf IMAP-Server in den Papierkorb + wird lokal gelöscht

Papierkorb-Ordner muss pro Mail-Konto konfiguriert werden:

  • Stalwart: Deleted Items
  • Ionos: Papierkorb
  • Gmail: [Gmail]/Trash
  • Standard/Dovecot: Trash

Entwicklung

cd apps/web
npm install
npm run dev     # Port 3000

cd services/worker
pip install -r requirements.txt
uvicorn main:app --reload --port 8000