📚 Doki — Administrations-Doku

Diese Doku beschreibt die Pflege und Wartung der Doki-Dokumentenverwaltung. Sie ergänzt die anleitung.html im Doki-Paket (Erst-Installation).

ℹ️ Erreichbarkeit: https://doki.rusti.ipv64.net
Hosting: LXC-Container 122-g50zeiten (192.168.178.122), parallel zu Stunden / Abos / Treckertreff
Datenbank: MariaDB, Datenbank doki_db, User doki_user
Storage: /var/doki-storage/ (außerhalb Web-Root)
Stand: 23. Mai 2026

Architektur — wer macht was?

Doki nutzt dieselbe Infrastruktur wie die anderen Apps auf .122. Eine Anfrage durchläuft folgende Stationen:

SchrittKomponenteWas passiert
1Browser/SmartphoneAufruf von https://doki.rusti.ipv64.net
2DNS (ipv64.net)Löst doki.rusti.ipv64.net in die Heim-IP auf
3FritzBoxPort 80/443 → 192.168.178.131 (NPM)
4NPM (192.168.178.131)Reverse-Proxy + Let's-Encrypt-SSL → 192.168.178.122:80
5Apache (.122)VirtualHost doki.conf liefert Dateien aus /var/www/doki/
6PHP / MariaDBapi.php spricht mit doki_db
7StorageDateien liegen in /var/doki-storage/ (außerhalb Web-Root!)
8Cron (alle 2 Min)ocr_worker.php verarbeitet OCR-Warteschlange
💡 Besonderheit: Doki ist die einzige der Apps mit asynchroner Hintergrund-Verarbeitung (OCR per Cronjob). Uploads sind dadurch sofort fertig, die Textextraktion läuft separat.

Datei-Layout auf .122

PfadInhalt
/var/www/doki/App-Code (PHP, HTML, JS, CSS), Besitzer www-data:www-data
/var/www/doki/index.htmlLogin-Seite
/var/www/doki/app.htmlHauptansicht (Sidebar + Liste/Kacheln + Modals)
/var/www/doki/api.phpBackend (Login, CRUD, Upload, Versionen, Download)
/var/www/doki/ocr_worker.phpCronjob-Skript für OCR
/var/www/doki/config.phpDB-Zugang, OCR-Pfade, App-Passwort-Hash
/var/www/doki/.htaccessApache-Schutzregeln
/var/doki-storage/Dokument-Dateien (PDFs, Bilder, Versionen), mode 750
/etc/apache2/sites-available/doki.confApache-VirtualHost
/var/log/apache2/doki_error.logApache-Error-Log
/var/log/apache2/doki_access.logApache-Access-Log
/var/log/doki-ocr.logOCR-Worker-Log (gefüllt vom Cronjob)

Datenbank-Struktur

Doki nutzt fünf Tabellen in doki_db:

TabelleInhalt
ordnerHierarchische Ordner-Struktur (id, name, parent_id, icon, farbe)
dokumenteHaupttabelle (titel, dateiname, mimetype, ordner_id, favorit, ocr_status, ocr_text, etc.)
dokument_versionenAlte Versionen (max 5 pro Dokument)
tagsTag-Definitionen (id, name, farbe)
dokument_tagsn:m-Zuordnung Dokumente ↔ Tags

FULLTEXT-Index für Suche

Auf der Tabelle dokumente liegt ein FULLTEXT-Index über titel + ocr_text + notizen. Damit funktioniert die Volltextsuche performant auch bei tausenden Dokumenten.

Features der App

📤 Multi-UploadMehrere Dateien gleichzeitig per Cmd-Klick oder Drag&Drop
📁 OrdnerHierarchisch, mit eigenen Icons + Farben
🏷️ TagsMehrere Tags pro Dokument, farbig markiert
⭐ FavoritenSchnellzugriff auf wichtige Dokumente
🔍 VolltextsucheDurchsucht Titel, OCR-Text und Notizen
📋 OCRTesseract (deutsch) + pdftotext für PDFs/Bilder
🔄 VersionierungBis zu 5 Versionen pro Dokument
👁️ VorschauPDFs/Bilder direkt im Browser ansehen
📱 MobileResponsive, Hamburger-Menü auf iPhone/Android
🎨 AnsichtenListe oder Kacheln, Wahl wird gespeichert
📊 StatistikSpeicherverbrauch, OCR-Status, Anzahl
📥 PWAAls App auf Homescreen installierbar

OCR-Pipeline im Detail

Doki nutzt einen zweistufigen Ansatz, um sowohl text-basierte PDFs als auch reine Bild-Scans zu verarbeiten:

Schritt 1 — pdftotext (für PDFs mit Text-Layer)
Schnell. Extrahiert eingebetteten Text direkt aus dem PDF. Wenn dabei < 50 Zeichen rauskommen, wird Schritt 2 versucht.
Schritt 2 — Tesseract (für gescannte PDFs und Bilder)
Fallback: PDF wird mit pdftoppm zu PNG umgewandelt (max 10 Seiten, 200 dpi), dann auf jeder Seite Tesseract mit deutscher Sprache.

Dependencies (sind installiert)

tesseract-ocr        # OCR-Engine
tesseract-ocr-deu    # Deutsche Sprache
poppler-utils        # pdftotext + pdftoppm

Worker-Flow

  1. Cron startet /var/www/doki/ocr_worker.php alle 2 Minuten
  2. Lockfile /tmp/doki-ocr.lock verhindert parallele Läufe
  3. Holt 1 Dokument mit ocr_status = 'pending'
  4. Setzt Status auf running
  5. Extrahiert Text (Schritt 1 oder 2)
  6. Speichert Text in dokumente.ocr_text, Status auf done
💡 Pro Worker-Lauf wird nur EIN Dokument verarbeitet — damit der Cron-Job nicht ewig läuft. Bei 30 neuen Dokumenten dauert die Verarbeitung also bis zu 1 Stunde.

App-Passwort ändern

Das Passwort wird als bcrypt-Hash in config.php gespeichert.

1 Per SSH einloggen
ssh root@192.168.178.122
2 Neues Passwort wählen (mit URL-Encoding!)
read -s -p "Neues Doki-Passwort: " PASSWORT
echo ""

PW_ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$PASSWORT")

HASH=$(curl -s "http://localhost/api.php?aktion=hash_erzeugen&pw=$PW_ENCODED" \
  -H "Host: doki.rusti.ipv64.net" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['hash'])")

echo "Hash-Länge: ${#HASH}"   # sollte 60 zeigen
3 Hash in config.php einsetzen
sed -i "s|APP_PASSWORT_HASH', '[^']*'|APP_PASSWORT_HASH', '$HASH'|" \
  /var/www/doki/config.php
4 Login testen
curl -s -X POST "http://localhost/api.php" \
  -H "Host: doki.rusti.ipv64.net" \
  -d "aktion=login&passwort=$PW_ENCODED"

# Erwartet: {"erfolg":true}
5 Aufräumen
unset PASSWORT HASH PW_ENCODED
⚠️ URL-Encoding ist Pflicht! Sonderzeichen wie $ & ? ! im Passwort müssen URL-encoded werden, sonst kommt das Passwort nicht korrekt im API an und ein falscher Hash wird generiert.

Backup erstellen

Wichtig: Doki hat zwei Backup-Aspekte!

  1. Datenbank (Metadaten, OCR-Text, Tags, etc.)
  2. Storage-Verzeichnis (die echten Dokumente!)

Komplettes Backup

# 1. Datenbank
mariadb-dump doki_db > /root/doki_db_$(date +%Y%m%d).sql

# 2. Storage (Dokumente!)
tar -czf /root/doki_storage_$(date +%Y%m%d).tar.gz /var/doki-storage/

# 3. App-Code (sicherheitshalber)
tar -czf /root/doki_code_$(date +%Y%m%d).tar.gz /var/www/doki/

# Größen anzeigen
ls -lh /root/doki_*
ℹ️ Proxmox sichert die VM automatisch. Damit sind sowohl DB als auch Storage in den Proxmox-Backups enthalten. Die manuellen Backups sind nützlich für:
  • Vor risikoreichen Änderungen
  • Migration auf einen anderen Server
  • Schnelle Wiederherstellung einzelner Dateien

Backup einspielen

Datenbank zurückspielen

# Aktuelle DB sichern (Notfall-Backup)
mariadb-dump doki_db > /root/doki_alt.sql

# DB neu aufbauen und Backup einspielen
mariadb -e "DROP DATABASE IF EXISTS doki_db; \
            CREATE DATABASE doki_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; \
            GRANT ALL PRIVILEGES ON doki_db.* TO 'doki_user'@'localhost';"

mariadb doki_db < /root/doki_db_20260523.sql

# Verifizieren
mariadb doki_db -e "SHOW TABLES; SELECT COUNT(*) FROM dokumente;"

Storage zurückspielen

# Storage leeren und neu befüllen
rm -rf /var/doki-storage/*
tar -xzf /root/doki_storage_20260523.tar.gz -C /

# Berechtigungen sicherstellen
chown -R www-data:www-data /var/doki-storage/
chmod 750 /var/doki-storage/
🚨 Wichtig: Datenbank UND Storage müssen aus dem gleichen Backup-Zeitpunkt stammen! Sonst zeigt die DB Dokumente an, die im Storage fehlen (oder umgekehrt).

OCR manuell anstoßen

Falls der Cron-Job mal hängt oder du sofort verarbeiten willst:

# Einen Lauf (verarbeitet 1 Dokument)
php /var/www/doki/ocr_worker.php

# Alle pending Dokumente abarbeiten
while [ $(mariadb doki_db -sN -e "SELECT COUNT(*) FROM dokumente WHERE ocr_status='pending'") -gt 0 ]; do
    php /var/www/doki/ocr_worker.php
done

# Status checken
mariadb doki_db -e "SELECT id, titel, ocr_status, LENGTH(ocr_text) AS text_len FROM dokumente;"

Speicherplatz prüfen

# Gesamte VM-Disk
df -h /

# Wo liegt wieviel?
du -sh /var/www/doki/         # App-Code (klein)
du -sh /var/doki-storage/     # Dokumente (wächst)
du -sh /var/lib/mysql/doki_db/  # Datenbank

# Top 10 größte Dokumente
ls -lhS /var/doki-storage/ | head -11

# Wie viele Versionen liegen rum?
mariadb doki_db -e "SELECT COUNT(*) AS versionen, SUM(dateigroesse)/1024/1024 AS MB_versionen FROM dokument_versionen;"

Code einspielen

Wenn eine neue Version einer Datei kommt:

1 Datei herunterladen (im Chat)
2 Vom Mac hochladen
cd ~/Downloads
scp app.html root@192.168.178.122:/var/www/doki/
3 Berechtigungen setzen
ssh root@192.168.178.122 "chown www-data:www-data /var/www/doki/*"
4 Im Browser: Hard-Reload

Cmd+Shift+R (Mac) oder Strg+F5 (Windows)

SSL-Zertifikat

Erneuerung läuft automatisch über NPM. Bei Bedarf manuell:

  1. NPM-UI: http://192.168.178.131:81
  2. Hosts → Proxy Hosts → doki.rusti.ipv64.net → Edit
  3. SSL-Tab → "Request a new SSL Certificate" → Save

Logs prüfen

BefehlZweck
tail -f /var/log/apache2/doki_access.logLive alle Zugriffe
tail -f /var/log/apache2/doki_error.logLive alle Fehler
tail -f /var/log/doki-ocr.logOCR-Worker-Aktivität
grep CRON /var/log/syslog | grep dokiCron-Job-Aufrufe

PHP-Fehler sichtbar machen (Debug)

In api.php ganz oben temporär einfügen:

ini_set('display_errors', '1');
error_reporting(E_ALL);

⚠️ Nach dem Debuggen wieder entfernen — sonst werden Pfade/Internas im Browser sichtbar.

API direkt testen

Login simulieren

# URL-encoded für Sonderzeichen
PW_ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "DEINPASSWORT")

curl -c /tmp/doki_cookies.txt -s -X POST "http://localhost/api.php" \
  -H "Host: doki.rusti.ipv64.net" \
  -d "aktion=login&passwort=$PW_ENCODED"

# Erwartet: {"erfolg":true}

Dokumente abfragen

curl -b /tmp/doki_cookies.txt -s \
  "http://localhost/api.php?aktion=dokumente_liste" \
  -H "Host: doki.rusti.ipv64.net" | python3 -m json.tool | head -40

Statistik

curl -b /tmp/doki_cookies.txt -s \
  "http://localhost/api.php?aktion=statistik" \
  -H "Host: doki.rusti.ipv64.net" | python3 -m json.tool

Datenbank-Tests

# Alle Dokumente
mariadb doki_db -e "SELECT id, titel, ocr_status, LENGTH(ocr_text) AS text_len FROM dokumente;"

# OCR-Statistik
mariadb doki_db -e "SELECT ocr_status, COUNT(*) FROM dokumente GROUP BY ocr_status;"

# Speicher pro Ordner
mariadb doki_db -e "
SELECT o.name, COUNT(d.id) AS anzahl, ROUND(SUM(d.dateigroesse)/1024/1024, 2) AS MB
FROM ordner o LEFT JOIN dokumente d ON d.ordner_id = o.id
GROUP BY o.id ORDER BY MB DESC;"

# Beispiel: OCR-Text durchsuchen
mariadb doki_db -e "SELECT id, titel, SUBSTRING(ocr_text, 1, 300) AS auszug
                    FROM dokumente WHERE titel LIKE '%Rechnung%';"

OCR debuggen

Tesseract-Test mit einem Bild

# Auf einer Test-Datei
tesseract -l deu /var/doki-storage/IRGENDEINE.png /tmp/test
cat /tmp/test.txt

PDF-Test mit pdftotext

pdftotext -layout /var/doki-storage/IRGENDEINE.pdf - | head -20

Worker-Lauf mit Output

# Lockfile löschen falls hängt
rm -f /tmp/doki-ocr.lock

# Worker mit Live-Output
php /var/www/doki/ocr_worker.php

Upload schlägt fehl

Symptom: "Keine Datei hochgeladen"

Bekannte Ursachen:

UrsacheDiagnoseFix
PHP-Upload-Limit zu klein curl http://localhost/phpinfo.php -H "Host: doki.rusti.ipv64.net" (siehe Kasten unten) In /etc/php/8.4/apache2/php.ini upload_max_filesize = 50M setzen + systemctl restart apache2
NPM blockt große Uploads NPM-Logs prüfen, oder 413 Request Entity Too Large In NPM Advanced-Tab: client_max_body_size 52m;
JS sendet GET statt POST Apache-Access-Log zeigt GET /api.php?aktion=upload mit 400 Behoben in v1.0 — `api()`-Helper erkennt FormData korrekt
phpinfo.php zum Debug erzeugen:
cat > /var/www/doki/phpinfo.php <<'EOF'
<?php
header('Content-Type: text/plain');
echo "upload_max_filesize: " . ini_get('upload_max_filesize') . "\n";
echo "post_max_size: " . ini_get('post_max_size') . "\n";
echo "memory_limit: " . ini_get('memory_limit') . "\n";
EOF
chown www-data:www-data /var/www/doki/phpinfo.php

curl -s "http://localhost/phpinfo.php" -H "Host: doki.rusti.ipv64.net"

# Nach dem Test wieder löschen!
rm /var/www/doki/phpinfo.php

UTF-8-Encoding ("🧾 Rechnungen" statt "🧾 Rechnungen")

Tritt auf, wenn setup.sql mit falschem Encoding eingespielt wurde — Doppel-UTF-8.

Diagnose

mariadb doki_db -e "SELECT name, HEX(name), HEX(icon) FROM ordner;"

Korrekt: ä = C3 A4. Falsch: C3 83 C2 A4.

Fix

mariadb doki_db <<'EOF'
UPDATE ordner SET name  = CONVERT(CAST(CONVERT(name  USING latin1) AS BINARY) USING utf8mb4);
UPDATE ordner SET icon  = CONVERT(CAST(CONVERT(icon  USING latin1) AS BINARY) USING utf8mb4);
UPDATE ordner SET farbe = CONVERT(CAST(CONVERT(farbe USING latin1) AS BINARY) USING utf8mb4);
UPDATE tags   SET name  = CONVERT(CAST(CONVERT(name  USING latin1) AS BINARY) USING utf8mb4);
UPDATE tags   SET farbe = CONVERT(CAST(CONVERT(farbe USING latin1) AS BINARY) USING utf8mb4);
EOF
💡 Terminal zeigt "?": Das ist nur ein Anzeigeproblem — die Daten in der DB sind korrekt. Browser zeigt's richtig an.

OCR steht still ("OCR WARTET" verschwindet nicht)

1. Cron läuft?

crontab -l | grep doki

Sollte zeigen: */2 * * * * /usr/bin/php /var/www/doki/ocr_worker.php >> /var/log/doki-ocr.log 2>&1

2. Was sagt das Log?

tail -30 /var/log/doki-ocr.log

3. Lockfile hängt?

ls -la /tmp/doki-ocr.lock
# Falls vorhanden und älter als 1 Stunde:
rm /tmp/doki-ocr.lock

4. Manueller Test

php /var/www/doki/ocr_worker.php

Browser-Cache-Probleme

BrowserHard-Reload
Chrome / Edge / Firefox (Mac)Cmd+Shift+R
Chrome / Edge (Windows)Strg+F5 oder Strg+Shift+R
Safari (Mac)Cmd+Option+E dann Cmd+R
iPhone SafariEinstellungen → Apps → Safari → Verlauf und Websitedaten löschen
📱 PWA-Cache auf iPhone: Wenn Doki als App auf dem Homescreen liegt, hat sie ihren EIGENEN Cache. Bei größeren Updates: App löschen, Safari-Cache leeren, dann neu zum Homescreen hinzufügen.

Mobile-Layout greift nicht

Doki wechselt bei ≤ 900px Breite auf Mobile-Layout.

Diagnose im Browser

  1. Browser-Fenster manuell auf 400px Breite ziehen
  2. Erwartet: Hamburger-Menü erscheint, Sidebar versteckt

Falls iPhone-Statusleiste das Suchfeld verdeckt

Behoben in v1.0 mit Safe-Area-Insets. Falls wieder auftaucht:

grep -c "safe-area-inset-top" /var/www/doki/app.html
# Sollte mindestens 3 zeigen

Sicherheitskonzept

SchutzWie
VerschlüsselungHTTPS via Let's Encrypt (NPM), Force-SSL aktiv
Brute-Force5 Versuche → 15 Min Sperre
Passwort-Speicherungbcrypt (Cost 12), niemals Klartext
SessionsCookie-Name DOKI_SESS, HttpOnly + SameSite=Lax, 7 Tage Lifetime
SQL-InjectionAlle DB-Zugriffe über PDO Prepared Statements
config.phpPer Apache <FilesMatch> blockiert
Storage außerhalb Web-Root/var/doki-storage/ nicht direkt aufrufbar — nur über api.php?aktion=download mit Login-Check
Datei-BerechtigungenStorage 750, Owner www-data
ClickjackingHTTP-Header X-Frame-Options: SAMEORIGIN
MIME-SniffingHTTP-Header X-Content-Type-Options: nosniff
Upload-GrößeLimitRequestBody 52428800 in Apache + PHP-Limits

Dateinamen werden randomisiert

Beim Upload wird der ursprüngliche Dateiname (z.B. "Rechnung.pdf") durch einen sicheren Namen ersetzt:

20260523_142611_a3f9c8d4b2e1f5a7.pdf

Format: Datum_Zeit_16HexZeichen.Endung. Damit:

Notfall-Wiederherstellung

Doki ist komplett down

systemctl status apache2 mariadb
tail -50 /var/log/apache2/doki_error.log
apache2ctl configtest
systemctl restart apache2
systemctl restart mariadb

Datenbank kaputt — Backup einspielen

Siehe Abschnitt Backup einspielen.

Storage gelöscht — Dokumente weg

Backup aus Proxmox-Snapshot zurückholen ODER aus tar-Archiv (siehe oben).

🚨 Achtung: Proxmox-Backup-Rollback der VM betrifft AUCH Stunden, Abos UND Treckertreff, weil alle 4 Apps auf der gleichen VM laufen! Für Doki-spezifische Wiederherstellung: nur Datenbank und Storage zurückspielen, nicht die ganze VM.

VM neu starten

Im Proxmox-Web-UI:

  1. VM 122-g50zeiten auswählen
  2. Oben rechts auf Neustart
  3. ~30 Sekunden warten, alle 4 Apps sind dann wieder erreichbar

Verwandte Dokus

Doki Admin-Doku · Stand 23.05.2026
https://doki.rusti.ipv64.net auf VM 122-g50zeiten
📚 Dokumenten-Archiv mit OCR + Volltextsuche