📋 Stundenabrechnung — Administrations-Doku
Diese Doku beschreibt die Pflege und Wartung der Stundenabrechnungs-App
auf der VM 122-g50zeiten (nach der Migration von IONOS).
https://stundng50.rusti.ipv64.netHosting: VM
122-g50zeiten (192.168.178.122)Datenbank: MariaDB, Datenbank
stunden_db, User stunden_userStand: 21. Mai 2026
Architektur — wer macht was?
Die Stundenabrechnung läuft auf der gleichen VM wie die Tretbeckenreinigungs-App, aber unter einer eigenen Subdomain. Eine Anfrage durchläuft folgende Stationen:
| Schritt | Komponente | Was passiert |
|---|---|---|
| 1 | Browser/Smartphone | Aufruf von https://stundng50.rusti.ipv64.net |
| 2 | DNS (ipv64.net) | Löst Subdomain in deine 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 → weiter an 192.168.178.122:80 |
| 5 | Apache (.122) | VirtualHost stunden.conf liefert Dateien aus /var/www/stunden/ |
| 6 | PHP / MariaDB | api.php liest und schreibt in stunden_db |
Datei-Layout auf .122
Alle App-Dateien liegen in /var/www/stunden/ und gehören www-data:www-data:
| Datei | Funktion |
|---|---|
index.html | Die Web-App (Oberfläche im Browser, mit JavaScript) |
api.php | Backend: Login, Einträge, Exports, Rechnungskalk. |
config.php | DB-Zugang, Passwort-Hash, Kundendaten, Stundensätze |
xlsx_export.php | Excel-Generator (eigene Engine ohne externe Bibliotheken) |
pdf_export.php | PDF-Generator (eigene Engine ohne externe Bibliotheken) |
setup.sql | Tabellen-Struktur (fĂĽr Erst-/Neuinstallation) |
.htaccess | Apache-Schutzregeln (blockt config.php etc.) |
favicon.svg | Browser-Tab-Icon (Geldsack) |
apple-touch-icon.png | iOS-Homescreen-Icon |
manifest.json | PWA-Manifest |
ANLEITUNG.md / anleitung.html | Original-Installations-Anleitung |
Apache-Konfiguration
Die VirtualHost-Datei liegt in:
/etc/apache2/sites-available/stunden.conf
Aktiviert mit a2ensite stunden.conf. Wichtige Punkte:
ServerName stundng50.rusti.ipv64.netDocumentRoot /var/www/stundenAllowOverride All(damit.htaccessgreift)- Logs:
/var/log/apache2/stunden_error.logundstunden_access.log
Datenbank-Struktur
Die App nutzt eine eigene MariaDB-Datenbank stunden_db mit einer Haupttabelle:
Tabelle stunden_eintraege
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INT PRIMARY KEY | Auto-Inkrement |
| datum | DATE | Arbeits-Datum |
| taetigkeit | TEXT | Beschreibung der Tätigkeit (mehrzeilig) |
| von | TIME | Arbeitsbeginn |
| bis | TIME | Arbeitsende |
| pause | TIME | Pausenzeit (wird abgezogen) |
| archiv | TIME | Davon Archiv-Stunden |
| erstellt_am / geaendert_am | TIMESTAMP |
App-Passwort ändern
Das Login-Passwort der App wird als bcrypt-Hash in config.php gespeichert.
ssh root@192.168.178.122
PASSWORT='NeuesPasswort2026'
HASH=$(curl -s "http://localhost/api.php?aktion=hash_erzeugen&pw=$PASSWORT" \
-H "Host: stundng50.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/stunden/config.php
curl -s -X POST http://localhost/api.php \
-H "Host: stundng50.rusti.ipv64.net" \
-d "aktion=login&passwort=$PASSWORT"
# Erwartung: {"erfolg":true}
unset PASSWORT HASH
hash_erzeugen-Endpunkt funktioniert ohne Login.
Sobald die App produktiv läuft, kannst du ihn theoretisch nutzen, aber besser nicht in der
URL-Leiste eintippen — das landet im Browser-Verlauf. Per curl auf der VM ist
am sichersten.
Kundendaten und Stundensätze ändern
In config.php sind Kunde, Stundensätze und weitere Konstanten hinterlegt:
nano /var/www/stunden/config.php
Welche Werte gibt's?
| Konstante | Bedeutung | Beispiel |
|---|---|---|
KUNDE_NAME | Erscheint im Excel/PDF-Kopf | Geno50 |
KUNDE_NR | Erscheint im Excel/PDF-Kopf | 69216 |
STD_VON | Standard-Arbeitsbeginn | 07:00 |
STD_BIS | Standard-Arbeitsende | 13:00 |
STD_PAUSE | Standard-Pausenzeit | 00:00 |
STD_TAETIGKEIT | Vorlagentext für Tätigkeit | Ext. Sicherung durchgeführt. |
Stundensätze ändern
Falls die Stundensätze für die Rechnungs-Kalkulation angepasst werden müssen, suche in
config.php nach den Preis-Konstanten und passe sie an:
# Beispiel - exakte Namen variieren je nach Code-Version:
define('PREIS_DIENSTLEISTUNG', 70.00);
define('PREIS_ARCHIV', 70.00);
define('PREIS_BEREITSTELLUNG', 80.00);
define('PAUSCHALE_STUNDEN', 20);
define('MAX_DIENSTLEISTUNG', 30);
define('MWST_PROZENT', 19);
grep -n "PREIS\|MWST\|PAUSCHALE" /var/www/stunden/config.php
findest du schnell die richtigen Zeilen.
Standardzeiten ändern
Die Standardwerte für neue Einträge werden in config.php gesetzt:
nano /var/www/stunden/config.php
# Diese Zeilen anpassen:
define('STD_VON', '07:00'); // Standard-Beginn
define('STD_BIS', '13:00'); // Standard-Ende
define('STD_PAUSE', '00:00'); // Standard-Pause
Speichern, dann Browser-Cache leeren (Cmd+Shift+R) — beim nächsten Eintrag werden die neuen Werte vorgeschlagen.
Backup erstellen
Datenbank-Backup
mariadb-dump stunden_db > /root/stunden_$(date +%Y%m%d).sql
ls -la /root/stunden_*.sql
Konfigurations-Backup
cp /var/www/stunden/config.php /root/stunden_config_$(date +%Y%m%d).php.bak
cp /etc/apache2/sites-available/stunden.conf /root/stunden.conf.bak
Vollständig: Tarball + DB-Dump
tar czf /root/stunden_full_$(date +%Y%m%d).tar.gz \
/var/www/stunden/ \
/etc/apache2/sites-available/stunden.conf
mariadb-dump stunden_db > /root/stunden_db_$(date +%Y%m%d).sql
ls -la /root/stunden_*
Backups auf den Mac herunterladen
scp root@192.168.178.122:/root/stunden_* ~/Backups/Stunden/
Backup einspielen
mariadb -e "DROP DATABASE IF EXISTS stunden_db; \
CREATE DATABASE stunden_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb stunden_db < /root/stunden_db_20260521.sql
mariadb stunden_db -e "SHOW TABLES; SELECT COUNT(*) FROM stunden_eintraege;"
cd /
tar xzf /root/stunden_full_20260521.tar.gz
chown -R www-data:www-data /var/www/stunden/
systemctl reload apache2
Jahresarchiv anlegen (am Jahresende)
Zum Jahreswechsel willst du eventuell die Daten archivieren, damit die App schneller bleibt. Das geht ĂĽber zwei Wege:
Variante 1: Daten in der DB lassen (empfohlen)
MariaDB ist schnell genug für tausende Einträge. Du musst gar nichts tun — die Monatsansicht filtert automatisch.
Variante 2: Datenbank-Snapshot als Archiv
# Komplettes Jahres-Backup erstellen
mariadb-dump stunden_db > /root/archiv/stunden_2026_komplett.sql
# Jahres-Daten exportieren (nur zur Sicherheit)
mariadb stunden_db -e "
SELECT * FROM stunden_eintraege
WHERE YEAR(datum) = 2026
ORDER BY datum;" > /root/archiv/stunden_2026.tsv
# Optional: alte Daten aus der aktiven DB löschen (nach Backup!)
# mariadb stunden_db -e "DELETE FROM stunden_eintraege WHERE YEAR(datum) = 2024;"
Code-Änderungen einspielen
Wenn ich (Claude) dir eine neue Version von Dateien schicke:
Aus dem Chat in den Downloads-Ordner.
Vom Mac:
cd ~/Downloads
scp index.html root@192.168.178.122:/var/www/stunden/
Vom Windows-PC (PowerShell):
cd $env:USERPROFILE\Downloads
scp index.html root@192.168.178.122:/var/www/stunden/
ssh root@192.168.178.122 "ls -la /var/www/stunden/index.html"
Wenn nicht www-data:www-data:
ssh root@192.168.178.122 "chown www-data:www-data /var/www/stunden/index.html"
Cmd+Shift+R (Mac) oder Strg+F5 (Windows).
SSL-Zertifikat
Das Zertifikat ist Let's-Encrypt und wird vom NPM auf 192.168.178.131 automatisch alle 60 Tage erneuert. Du musst normalerweise nichts tun.
Falls doch mal manuell:
- NPM-Web-UI öffnen:
http://192.168.178.131:81 - Hosts → Proxy Hosts →
stundng50.rusti.ipv64.net→ Edit - SSL-Tab → "Request a new SSL Certificate" → Save
Logs prĂĽfen
Apache-Logs
| Befehl | Zweck |
|---|---|
tail -f /var/log/apache2/stunden_access.log | Live alle Zugriffe |
tail -f /var/log/apache2/stunden_error.log | Live alle Fehler |
tail -50 /var/log/apache2/stunden_error.log | Letzte 50 Fehler |
grep -i "fatal\|error" /var/log/apache2/stunden_error.log | tail -20 | Nur Fehler |
PHP-Fehler sichtbar machen
Beim Debuggen kannst du PHP-Fehler direkt im Browser sehen. In api.php ganz oben einfĂĽgen:
ini_set('display_errors', '1');
error_reporting(E_ALL);
⚠️ Nach dem Debuggen wieder entfernen.
API direkt testen (ohne Browser)
Login simulieren
curl -c /tmp/stunden_cookies.txt -s -X POST http://localhost/api.php \
-H "Host: stundng50.rusti.ipv64.net" \
-d "aktion=login&passwort=DEINPASSWORT"
# Erwartung: {"erfolg":true}
Einträge eines Monats laden
curl -b /tmp/stunden_cookies.txt -s \
"http://localhost/api.php?aktion=eintraege_laden&jahr=2026&monat=5" \
-H "Host: stundng50.rusti.ipv64.net" | python3 -m json.tool
Letzten Eintrag holen
curl -b /tmp/stunden_cookies.txt -s \
"http://localhost/api.php?aktion=letzter_eintrag" \
-H "Host: stundng50.rusti.ipv64.net"
Excel-Export simulieren
curl -b /tmp/stunden_cookies.txt -s \
"http://localhost/api.php?aktion=excel_export&jahr=2026&monat=5" \
-H "Host: stundng50.rusti.ipv64.net" \
-o /tmp/test_export.xlsx
# PrĂĽfen ob's wirklich eine XLSX ist
file /tmp/test_export.xlsx
# Sollte zeigen: "Microsoft Excel 2007+"
Datenbank-Tests
Inhalt prĂĽfen
# Letzte 10 Einträge
mariadb stunden_db -e "SELECT id, datum, taetigkeit, von, bis, pause, archiv \
FROM stunden_eintraege \
ORDER BY datum DESC, id DESC LIMIT 10;"
# Anzahl pro Monat
mariadb stunden_db -e "SELECT YEAR(datum) AS jahr, MONTH(datum) AS monat, COUNT(*) AS anzahl \
FROM stunden_eintraege \
GROUP BY YEAR(datum), MONTH(datum) \
ORDER BY jahr, monat;"
Encoding prĂĽfen
mariadb stunden_db -e "SELECT taetigkeit, HEX(taetigkeit) FROM stunden_eintraege LIMIT 1;"
Bei korrektem UTF-8 ist ä = C3 A4 (2 Bytes). Falls 4 Bytes → siehe
Umlaute kaputt.
Internal Server Error (500)
Browser zeigt "500 Internal Server Error" → PHP-Fehler. Diagnose:
tail -30 /var/log/apache2/stunden_error.log
Häufige Ursachen:
- DB-Zugang falsch: PrĂĽfe
DB_PASSinconfig.php - MariaDB läuft nicht:
systemctl status mariadb - Syntax-Fehler in PHP:
php -l /var/www/stunden/api.php - Falsche Berechtigungen:
chown -R www-data:www-data /var/www/stunden/ - Fehler im Export: Bei XLSX/PDF-Export gibt's spezielle Fehler — siehe nächster Abschnitt
Login funktioniert nicht
1. Hash-Format prĂĽfen
python3 <<'EOF'
import re
with open('/var/www/stunden/config.php', 'r') as f:
content = f.read()
m = re.search(r"APP_PASSWORT_HASH'\s*,\s*'([^']+)'", content)
if m:
h = m.group(1)
print(f"Länge: {len(h)}")
print(f"Beginnt mit: {h[:7]}")
print(f"OK" if h.startswith('$2y$') and len(h) == 60 else "FALSCH")
EOF
Erwartung: Länge 60, beginnt mit $2y$10$ oder $2y$12$.
2. Brute-Force-Sperre aktiv?
Nach 5 Fehlversuchen wird die IP fĂĽr 15 Min gesperrt. Workaround:
# PHP-Sessions löschen (für ALLE User der VM!)
rm /var/lib/php/sessions/sess_*
# Browser-Cookie löschen oder Inkognito-Tab nutzen
3. Session-Cookie zu alt (Auto-Logout)
Nach 120 Minuten Inaktivität wirst du automatisch ausgeloggt. Einfach neu einloggen.
Excel/PDF-Export Probleme
PDF zeigt Fehlermeldung als Text
Die App fängt PDF-Fehler ab und zeigt sie als Klartext (statt einer kaputten PDF). Wenn du sowas siehst:
PDF-Export Fehler:
[Fehlertext]
Datei: /var/www/stunden/pdf_export.php
Zeile: 123
→ Datei und Zeilennummer notieren und mir geben, dann fixe ich das schnell.
Excel öffnet sich nicht in Numbers (Mac)
Apple Numbers ist bei Excel-Format pingelig:
- Datums-Format muss Kleinbuchstaben sein (
dd.mm.yyyy) - Stundenformate ĂĽber 24h werden als Dezimalwert ausgegeben (9,50 statt 9:30)
- Wenn trotzdem Probleme: in LibreOffice öffnen und neu als Excel speichern
Excel-Tabelle leer / fehlerhaft
Test direkt per API:
curl -b /tmp/stunden_cookies.txt -s \
"http://localhost/api.php?aktion=excel_export&jahr=2026&monat=5" \
-H "Host: stundng50.rusti.ipv64.net" \
-o /tmp/test.xlsx
file /tmp/test.xlsx
# Erwartung: Microsoft Excel 2007+
Wenn da was anderes steht (z.B. ASCII text), ist im Body ein Fehler. Mit head /tmp/test.xlsx ansehen.
Browser-Cache-Probleme
Symptom: Du hast was geändert, im Browser kommt aber noch die alte Version.
| Browser | Hard-Reload |
|---|---|
| Chrome / Edge (Windows) | Strg+F5 oder Strg+Shift+R |
| Chrome / Edge / Firefox (Mac) | Cmd+Shift+R |
| Safari (Mac) | Cmd+Option+E (Cache leeren) dann Cmd+R |
| Mobile Safari (iOS) | App schließen, Inkognito-Tab öffnen |
Umlaute kaputt
Bei Migration von IONOS könnten Umlaute doppelt UTF-8 kodiert sein. Diagnose:
mariadb stunden_db -e "SELECT taetigkeit, HEX(taetigkeit) FROM stunden_eintraege \
WHERE taetigkeit LIKE '%ä%' OR taetigkeit LIKE '%ö%' OR taetigkeit LIKE '%ü%' \
LIMIT 5;"
Bei korrektem UTF-8 ist ä = C3 A4 (2 Bytes). Bei kaputtem doppelt-UTF-8 ist es 4 Bytes.
Reparatur (nur wenn nötig!):
# Backup vorher!
mariadb-dump stunden_db > /tmp/stunden_backup.sql
# Reparatur
mariadb stunden_db <<'EOF'
UPDATE stunden_eintraege
SET taetigkeit = CONVERT(CAST(CONVERT(taetigkeit USING latin1) AS BINARY) USING utf8mb4);
EOF
Was schĂĽtzt die App?
| Schutz | Wie |
|---|---|
| VerschlĂĽsselung | HTTPS via Let's Encrypt (NPM), Force-SSL aktiv |
| Brute-Force | 5 Versuche → 15 Min Sperre |
| Passwort-Speicherung | bcrypt-Hash (Cost 10), niemals Klartext |
| Sessions | HttpOnly + SameSite=Strict, Auto-Logout nach 120 Min |
| SQL-Injection | Alle DB-Zugriffe nutzen Prepared Statements |
| config.php | Per Apache <FilesMatch> blockiert |
| Login-Protokoll | 7 Tage gespeichert |
| Doppel-Bestätigung beim Löschen | "LÖSCHEN" in Großbuchstaben tippen |
Apache-Sicherheits-Header hinzufĂĽgen (optional)
Falls noch nicht gesetzt, kannst du wie bei der Treckertreff-App die 3 Sicherheits-Header hinzufĂĽgen:
a2enmod headers
sed -i '/DocumentRoot/a\
\
# === Sicherheits-Header ===\
Header always set X-Frame-Options "SAMEORIGIN"\
Header always set X-Content-Type-Options "nosniff"\
Header always set Referrer-Policy "strict-origin-when-cross-origin"' \
/etc/apache2/sites-available/stunden.conf
apache2ctl configtest
systemctl reload apache2
# Verifizieren
curl -sI https://stundng50.rusti.ipv64.net/ | grep -iE "x-frame|x-content|referrer"
Notfall-Wiederherstellung
App ist komplett down
systemctl status apache2 mariadb
tail -50 /var/log/apache2/stunden_error.log
tail -50 /var/log/mysql/error.log
apache2ctl configtest
systemctl restart apache2
systemctl restart mariadb
Datenbank kaputt — Backup einspielen
# Aktuelle DB als Notfall-Sicherung
mariadb-dump stunden_db > /root/stunden_kaputt.sql
# ZurĂĽcksetzen und letztes Backup einspielen
mariadb -e "DROP DATABASE stunden_db; \
CREATE DATABASE stunden_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb stunden_db < /root/stunden_db_LETZTES.sql
Alles ist kaputt — VM-Rollback
Im Proxmox-Web-UI:
- VM
122-g50zeitenauswählen - Links auf "Backup"
- Letztes funktionierendes Backup auswählen
- "ZurĂĽckspielen" klicken
Verwandte Dokus
webserver-doku.html— DNS-Updates, ipv64, NPM, System-Updatestreckertreff-doku.html— Treckertreff-App auf gleicher VManleitung.html / ANLEITUNG.md(im Stunden-Paket) — Original-Installations-Anleitung
Stundenabrechnung Admin-Doku Stand: 21.05.2026
https://stundng50.rusti.ipv64.net auf VM 122-g50zeiten