📚 Doki — Administrations-Doku
Diese Doku beschreibt die Pflege und Wartung der Doki-Dokumentenverwaltung.
Sie ergänzt die anleitung.html im Doki-Paket (Erst-Installation).
https://doki.rusti.ipv64.netHosting: LXC-Container
122-g50zeiten (192.168.178.122), parallel zu Stunden / Abos / TreckertreffDatenbank: MariaDB, Datenbank
doki_db, User doki_userStorage:
/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:
| Schritt | Komponente | Was passiert |
|---|---|---|
| 1 | Browser/Smartphone | Aufruf von https://doki.rusti.ipv64.net |
| 2 | DNS (ipv64.net) | Löst doki.rusti.ipv64.net in die Heim-IP auf |
| 3 | FritzBox | Port 80/443 → 192.168.178.131 (NPM) |
| 4 | NPM (192.168.178.131) | Reverse-Proxy + Let's-Encrypt-SSL → 192.168.178.122:80 |
| 5 | Apache (.122) | VirtualHost doki.conf liefert Dateien aus /var/www/doki/ |
| 6 | PHP / MariaDB | api.php spricht mit doki_db |
| 7 | Storage | Dateien liegen in /var/doki-storage/ (außerhalb Web-Root!) |
| 8 | Cron (alle 2 Min) | ocr_worker.php verarbeitet OCR-Warteschlange |
Datei-Layout auf .122
| Pfad | Inhalt |
|---|---|
/var/www/doki/ | App-Code (PHP, HTML, JS, CSS), Besitzer www-data:www-data |
/var/www/doki/index.html | Login-Seite |
/var/www/doki/app.html | Hauptansicht (Sidebar + Liste/Kacheln + Modals) |
/var/www/doki/api.php | Backend (Login, CRUD, Upload, Versionen, Download) |
/var/www/doki/ocr_worker.php | Cronjob-Skript für OCR |
/var/www/doki/config.php | DB-Zugang, OCR-Pfade, App-Passwort-Hash |
/var/www/doki/.htaccess | Apache-Schutzregeln |
/var/doki-storage/ | Dokument-Dateien (PDFs, Bilder, Versionen), mode 750 |
/etc/apache2/sites-available/doki.conf | Apache-VirtualHost |
/var/log/apache2/doki_error.log | Apache-Error-Log |
/var/log/apache2/doki_access.log | Apache-Access-Log |
/var/log/doki-ocr.log | OCR-Worker-Log (gefüllt vom Cronjob) |
Datenbank-Struktur
Doki nutzt fünf Tabellen in doki_db:
| Tabelle | Inhalt |
|---|---|
ordner | Hierarchische Ordner-Struktur (id, name, parent_id, icon, farbe) |
dokumente | Haupttabelle (titel, dateiname, mimetype, ordner_id, favorit, ocr_status, ocr_text, etc.) |
dokument_versionen | Alte Versionen (max 5 pro Dokument) |
tags | Tag-Definitionen (id, name, farbe) |
dokument_tags | n: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
OCR-Pipeline im Detail
Doki nutzt einen zweistufigen Ansatz, um sowohl text-basierte PDFs als auch reine Bild-Scans zu verarbeiten:
Schnell. Extrahiert eingebetteten Text direkt aus dem PDF. Wenn dabei
< 50 Zeichen rauskommen, wird Schritt 2 versucht.
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
- Cron startet
/var/www/doki/ocr_worker.phpalle 2 Minuten - Lockfile
/tmp/doki-ocr.lockverhindert parallele Läufe - Holt 1 Dokument mit
ocr_status = 'pending' - Setzt Status auf
running - Extrahiert Text (Schritt 1 oder 2)
- Speichert Text in
dokumente.ocr_text, Status aufdone
App-Passwort ändern
Das Passwort wird als bcrypt-Hash in config.php gespeichert.
ssh root@192.168.178.122
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
sed -i "s|APP_PASSWORT_HASH', '[^']*'|APP_PASSWORT_HASH', '$HASH'|" \
/var/www/doki/config.php
curl -s -X POST "http://localhost/api.php" \
-H "Host: doki.rusti.ipv64.net" \
-d "aktion=login&passwort=$PW_ENCODED"
# Erwartet: {"erfolg":true}
unset PASSWORT HASH PW_ENCODED
$ & ? ! 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!
- Datenbank (Metadaten, OCR-Text, Tags, etc.)
- 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_*
- 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/
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:
cd ~/Downloads
scp app.html root@192.168.178.122:/var/www/doki/
ssh root@192.168.178.122 "chown www-data:www-data /var/www/doki/*"
Cmd+Shift+R (Mac) oder Strg+F5 (Windows)
SSL-Zertifikat
Erneuerung läuft automatisch über NPM. Bei Bedarf manuell:
- NPM-UI:
http://192.168.178.131:81 - Hosts → Proxy Hosts →
doki.rusti.ipv64.net→ Edit - SSL-Tab → "Request a new SSL Certificate" → Save
Logs prüfen
| Befehl | Zweck |
|---|---|
tail -f /var/log/apache2/doki_access.log | Live alle Zugriffe |
tail -f /var/log/apache2/doki_error.log | Live alle Fehler |
tail -f /var/log/doki-ocr.log | OCR-Worker-Aktivität |
grep CRON /var/log/syslog | grep doki | Cron-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:
| Ursache | Diagnose | Fix |
|---|---|---|
| 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 |
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
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
| Browser | Hard-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 Safari | Einstellungen → Apps → Safari → Verlauf und Websitedaten löschen |
Mobile-Layout greift nicht
Doki wechselt bei ≤ 900px Breite auf Mobile-Layout.
Diagnose im Browser
- Browser-Fenster manuell auf 400px Breite ziehen
- 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
| Schutz | Wie |
|---|---|
| Verschlüsselung | HTTPS via Let's Encrypt (NPM), Force-SSL aktiv |
| Brute-Force | 5 Versuche → 15 Min Sperre |
| Passwort-Speicherung | bcrypt (Cost 12), niemals Klartext |
| Sessions | Cookie-Name DOKI_SESS, HttpOnly + SameSite=Lax, 7 Tage Lifetime |
| SQL-Injection | Alle DB-Zugriffe über PDO Prepared Statements |
| config.php | Per Apache <FilesMatch> blockiert |
| Storage außerhalb Web-Root | /var/doki-storage/ nicht direkt aufrufbar — nur über api.php?aktion=download mit Login-Check |
| Datei-Berechtigungen | Storage 750, Owner www-data |
| Clickjacking | HTTP-Header X-Frame-Options: SAMEORIGIN |
| MIME-Sniffing | HTTP-Header X-Content-Type-Options: nosniff |
| Upload-Größe | LimitRequestBody 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:
- Keine Pfad-Traversal-Angriffe möglich
- Keine Namens-Kollisionen
- Original-Name bleibt in DB als
dateiname_originalerhalten
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).
VM neu starten
Im Proxmox-Web-UI:
- VM
122-g50zeitenauswählen - Oben rechts auf Neustart
- ~30 Sekunden warten, alle 4 Apps sind dann wieder erreichbar
Verwandte Dokus
webserver-doku.html— DNS-Updates, ipv64, NPM, System-Updatesstunden-doku.html— Stundenabrechnung-App auf gleicher VMtreckertreff-doku.html— Treckertreff-App auf gleicher VMabos-doku.html— Abo-Verwaltung auf gleicher VManleitung.html(im Doki-Paket) — Original-Installations-Anleitung
Doki Admin-Doku · Stand 23.05.2026
https://doki.rusti.ipv64.net auf VM 122-g50zeiten
📚 Dokumenten-Archiv mit OCR + Volltextsuche