- Svelte 57.9%
- TypeScript 20.3%
- Python 20%
- HTML 0.9%
- Shell 0.8%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github | ||
| apps/web | ||
| scripts | ||
| services | ||
| .env.example | ||
| .gitignore | ||
| CLA.md | ||
| docker-compose.yml | ||
| license.md | ||
| migrate_mail_trash.sql | ||
| README.md | ||
| RELEASE.md | ||
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.sqllä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:
ORIGINleer lassen → App startet nichtORIGINauf.desetzen → 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
- Operator startet Scan über Agent-UI (Port 8089 auf Scanner-PC)
- Agent scannt ADF, führt OCR durch, lädt PDF hoch
- Job erscheint im Dashboard mit Live-Status
- Zwei Indexierer erfassen dasselbe Dokument blind (unabhängig voneinander)
- System vergleicht: stimmt alles →
auto_approved, sonst →conflict - Konflikte gehen in die Merger-Queue (
/merge) - Nach Freigabe aller Dokumente: Admin exportiert → PDF + CSV
Dokument-Status-Flow
pending → ocr_done → entry_1_done ──→ auto_approved ──→ exported
└──────→ conflict ──→ merged ──┘
Mail-Eingang
- Fetcher pollt IMAP-Konten alle 15s, speichert PDF-Anhänge lokal
- Admin öffnet Mail-Eingang (
/admin/mail-inbox) - Pro Anhang: bestehende oder neue Position auswählen → Import
- Wenn alle Anhänge einer Mail verarbeitet:
🗑 Löschen-Button erscheint - 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