💰 Abo-Verwaltung — Administrations-Doku
Diese Doku beschreibt die Pflege und Wartung der Abo-Verwaltungs-App.
Sie ergänzt die anleitung.html im Abo-Paket (Erst-Installation).
https://abos.rusti.ipv64.netHosting: VM
122-g50zeiten (192.168.178.122), parallel zu Stunden und TreckertreffDatenbank: MariaDB, Datenbank
abos_db, User abos_userStand: 22. Mai 2026
Architektur — wer macht was?
Die App nutzt dieselbe Infrastruktur wie Stundenabrechnung und Treckertreff. Eine Anfrage durchläuft folgende Stationen:
| Schritt | Komponente | Was passiert |
|---|---|---|
| 1 | Browser/Smartphone | Aufruf von https://abos.rusti.ipv64.net |
| 2 | DNS (ipv64.net) | Löst abos.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 abos.conf liefert Dateien aus /var/www/abos/ |
| 6 | PHP / MariaDB | api.php spricht mit abos_db |
ServerName gesetzt sein. Fehlt er, fällt Apache auf
die alphabetisch erste Site zurück. Siehe Subdomain zeigt falsche App.
Datei-Layout auf .122
Alle App-Dateien liegen in /var/www/abos/ und gehören www-data:www-data:
| Datei | Funktion |
|---|---|
index.html | Login-Seite (dunkles Design) |
abo_manager.html | Haupt-App: Desktop-Tabelle + Mobile-Cards |
api.php | Backend: Login, CRUD, Backup-Import/Export |
config.php | DB-Zugang, App-Passwort-Hash |
setup.sql | Tabellen-Struktur |
.htaccess | Apache-Schutzregeln |
favicon.svg | Browser-Tab-Icon (Karten-Stapel mit Euro) |
favicon-32.png | 32×32 PNG-Fallback |
apple-touch-icon.png | iOS-Homescreen-Icon (180×180) |
icon-192.png / icon-512.png | Android-PWA-Icons |
manifest.json | PWA-Manifest |
anleitung.html | Installations-Anleitung |
Apache-Konfiguration
/etc/apache2/sites-available/abos.conf
Wichtige Punkte:
ServerName abos.rusti.ipv64.net(zwingend!)DocumentRoot /var/www/abos- 3 Sicherheits-Header (X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- Logs:
/var/log/apache2/abos_error.logundabos_access.log
Datenbank-Struktur
Die App nutzt eine einzige Tabelle:
Tabelle abos
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | VARCHAR(40) PRIMARY KEY | UUID-artige ID (clientseitig generiert) |
| name | VARCHAR(200) | z.B. "Spotify", "Office 365" |
| ekPrice | DECIMAL(10,2) | Einkaufspreis (Brutto netto, je nach Setup) |
| vkPrice | DECIMAL(10,2) | Verkaufspreis |
| interval_typ | VARCHAR(30) | weekly / monthly / quarterly / biannual / yearly / biennial / triennial |
| nextDate | DATE | Nächster Fälligkeitstermin |
| category | VARCHAR(50) | Kategorie (frei wählbar) |
| customer | VARCHAR(200) | Kunde / Eigentümer |
| notes | TEXT | Freitext-Notizen |
| eolDate | DATE NULL | End-of-Life Datum (optional) |
| contractEnd | DATE NULL | Vertragsende (optional) |
| cancelDays | INT | Kündigungsfrist in Tagen |
| erstellt_am / geaendert_am | TIMESTAMP | Automatisch |
Features der App
| Feature | Beschreibung |
|---|---|
| 📊 Statistik-Kacheln | Anzahl Abos, Monatskosten, Jahreskosten, Marge (gesperrt) |
| 📅 Termin-Übersicht | Sortiert nach nächster Fälligkeit, farbige Status (überfällig / heute / bald / später) |
| 🔒 Marge-Schutz | EK-Preis + Marge sind versteckt — Anzeige nur nach Login-Eingabe |
| 🔍 Suche & Filter | Volltextsuche, EOL-Filter, Kategorie-Filter |
| 📱 Mobile-Cards | Auf Smartphone Karten statt Tabelle (< 768px Breite) |
| 📆 ICS-Export | Kalendereinträge pro Abo (für Erinnerung) |
| 📄 PDF-Export | Firmen-Export (ohne Marge) und Händler-Export (mit Marge) |
| 💾 JSON-Backup | Download + Wiederherstellen (Merge oder Replace) |
App-Passwort ändern
Das Passwort 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: abos.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/abos/config.php
curl -s -X POST http://localhost/api.php \
-H "Host: abos.rusti.ipv64.net" \
-d "aktion=login&passwort=$PASSWORT"
# Erwartung: {"erfolg":true}
unset PASSWORT HASH
Backup erstellen
Variante 1: Per Browser (einfach)
- App öffnen, einloggen
- Auf ↓ Backup klicken
- JSON-Datei landet im Downloads-Ordner
Variante 2: Direkt auf der VM
mariadb-dump abos_db > /root/abos_$(date +%Y%m%d).sql
ls -la /root/abos_*.sql
Variante 3: Über die API
curl -b /tmp/abos_cookies.txt -s \
"http://localhost/api.php?aktion=backup_export" \
-H "Host: abos.rusti.ipv64.net" \
-o /root/abos_backup_$(date +%Y%m%d).json
- Einzelne Daten wiederherstellen (z.B. versehentlich gelöschter Eintrag)
- Daten auf einen anderen Server kopieren
- Vor risikoreichen Änderungen
JSON-Backup importieren
So bekommst du Daten aus einer JSON-Sicherung wieder in die App:
- OK = REPLACE: Alle bestehenden Daten werden gelöscht, dann das Backup eingespielt. Geeignet für komplettes Reset / Migration.
- Abbrechen = MERGE: Nur neue Einträge dazu, bestehende mit gleicher ID werden aktualisiert. Geeignet wenn du nur einzelne fehlende Einträge wieder reinholen willst.
Datenbank-Backup einspielen
Wenn du ein SQL-Dump (nicht JSON) hast:
# Aktuelle DB sichern (Notfall-Backup)
mariadb-dump abos_db > /root/abos_alt_$(date +%Y%m%d).sql
# Datenbank leeren und Backup einspielen
mariadb -e "DROP DATABASE IF EXISTS abos_db; \
CREATE DATABASE abos_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; \
GRANT ALL PRIVILEGES ON abos_db.* TO 'abos_user'@'localhost';"
mariadb abos_db < /root/abos_db_20260522.sql
# Verifizieren
mariadb abos_db -e "SHOW TABLES; SELECT COUNT(*) FROM abos;"
Code-Änderungen einspielen
Wenn ich (Claude) dir eine neue Version einer Datei schicke:
Vom Mac:
cd ~/Downloads
scp abo_manager.html root@192.168.178.122:/var/www/abos/
ssh root@192.168.178.122 "chown www-data:www-data /var/www/abos/*"
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 →
abos.rusti.ipv64.net→ Edit - SSL-Tab → "Request a new SSL Certificate" → Save
Logs prüfen
| Befehl | Zweck |
|---|---|
tail -f /var/log/apache2/abos_access.log | Live alle Zugriffe |
tail -f /var/log/apache2/abos_error.log | Live alle Fehler |
tail -50 /var/log/apache2/abos_error.log | Letzte 50 Fehler |
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.
API direkt testen
Login simulieren
curl -c /tmp/abos_cookies.txt -s -X POST http://localhost/api.php \
-H "Host: abos.rusti.ipv64.net" \
-d "aktion=login&passwort=DEINPASSWORT"
# Erwartung: {"erfolg":true}
Alle Abos laden
curl -b /tmp/abos_cookies.txt -s \
"http://localhost/api.php?aktion=liste_laden" \
-H "Host: abos.rusti.ipv64.net" | python3 -m json.tool | head -30
Backup-Export
curl -b /tmp/abos_cookies.txt -s \
"http://localhost/api.php?aktion=backup_export" \
-H "Host: abos.rusti.ipv64.net" \
-o /tmp/test_backup.json
ls -la /tmp/test_backup.json
head -5 /tmp/test_backup.json
Datenbank-Tests
Inhalte prüfen
# Alle Abos
mariadb abos_db -e "SELECT id, name, vkPrice, interval_typ, nextDate FROM abos;"
# Anzahl + Summe
mariadb abos_db -e "SELECT COUNT(*) AS anzahl, SUM(vkPrice) AS summe_vk FROM abos;"
# Demnächst fällig (nächste 30 Tage)
mariadb abos_db -e "SELECT name, nextDate, DATEDIFF(nextDate, CURDATE()) AS in_tagen \
FROM abos WHERE nextDate <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) \
ORDER BY nextDate;"
Subdomain zeigt falsche App
Klassischer Apache-VirtualHost-Bug: Du rufst abos.rusti.ipv64.net auf,
siehst aber die Stundenabrechnung (oder umgekehrt).
Diagnose
apachectl -t -D DUMP_VHOSTS
Erwartung — jede Site hat ihren eigenen ServerName:
port 80 namevhost abos.rusti.ipv64.net (.../abos.conf:1)
port 80 namevhost stundng50.rusti.ipv64.net (.../stunden.conf:1)
port 80 namevhost tbrt.rusti.ipv64.net (.../treckertreff.conf:1)
Wenn dort statt stundng50.rusti.ipv64.net nur g50zeiten steht
(Hostname der VM), fehlt der ServerName in der Config!
Fix
cat /etc/apache2/sites-available/stunden.conf | head -5
# Falls ServerName fehlt:
sed -i '/<VirtualHost \*:80>/a\ ServerName stundng50.rusti.ipv64.net' \
/etc/apache2/sites-available/stunden.conf
apache2ctl configtest
systemctl reload apache2
Host:-Headers. Wenn keine Site einen passenden ServerName hat,
nimmt Apache die alphabetisch erste — bei uns abos.conf. Deshalb landest du
immer auf der Abo-App, egal welche Subdomain du eingibst.
Login funktioniert nicht
1. Hash-Format prüfen
python3 <<'EOF'
import re
with open('/var/www/abos/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 Session für 15 Min gesperrt. Workaround:
rm /var/lib/php/sessions/sess_*
# Browser-Cookie löschen oder Inkognito-Tab
3. Test direkt mit password_verify
HASH='HIER_DEINEN_HASH_EINSETZEN'
PASSWORT='HIER_DEIN_PASSWORT'
php -r "echo password_verify('$PASSWORT', '$HASH') ? 'PASST' : 'PASST NICHT';"
Marge-Anzeige Probleme
Symptom: Du klickst auf das Schloss-Symbol, gibst Passwort ein, aber Margen werden nicht angezeigt.
Was passiert intern?
Beim Marge-Entsperren wird das eingegebene Passwort an api.php?aktion=login geschickt.
Wenn die Antwort {"erfolg":true} ist, wird marginUnlocked = true gesetzt
und die Tabelle/Karten werden neu gerendert.
Diagnose im Browser
- F12 öffnet Developer Tools
- Tab Network öffnen
- Schloss-Symbol klicken → Passwort eingeben
- Den
api.php-Request anschauen — Status sollte 200 sein, Antwort{"erfolg":true}
Falls Fehler: Passwort prüfen (das gleiche wie für Login).
Internal Server Error (500)
tail -30 /var/log/apache2/abos_error.log
Häufige Ursachen:
- DB-Zugang falsch: Prüfe
DB_PASSinconfig.php - MariaDB läuft nicht:
systemctl status mariadb - Syntax-Fehler:
php -l /var/www/abos/api.php - Falsche Berechtigungen:
chown -R www-data:www-data /var/www/abos/
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 / iPad | Safari schließen, Inkognito-Tab |
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 (Cost 10/12), niemals Klartext |
| Sessions | Cookie-Name ABOS_SESS, HttpOnly + SameSite=Lax, 7 Tage Lifetime |
| SQL-Injection | Alle DB-Zugriffe über Prepared Statements |
| config.php | Per Apache <FilesMatch> blockiert |
| Clickjacking | HTTP-Header X-Frame-Options: SAMEORIGIN |
| MIME-Sniffing | HTTP-Header X-Content-Type-Options: nosniff |
| Referrer-Leak | HTTP-Header Referrer-Policy: strict-origin-when-cross-origin |
| Marge-Schutz | EK-Preis nur nach erneuter Login-Eingabe sichtbar |
Notfall-Wiederherstellung
App ist komplett down
systemctl status apache2 mariadb
tail -50 /var/log/apache2/abos_error.log
apache2ctl configtest
systemctl restart apache2
systemctl restart mariadb
Datenbank kaputt — Backup einspielen
mariadb-dump abos_db > /root/abos_kaputt.sql
mariadb -e "DROP DATABASE abos_db; \
CREATE DATABASE abos_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb abos_db < /root/abos_db_LETZTES.sql
Versehentlich Einträge gelöscht — aus JSON wiederherstellen
JSON-Backup (Browser-Download) in die App importieren mit "↑ Wiederherstellen" und Modus MERGE (Abbrechen) wählen. Damit kommen nur die fehlenden Einträge wieder rein, ohne neuere zu überschreiben.
VM-Rollback (letzte Option)
Im Proxmox-Web-UI:
- VM
122-g50zeitenauswählen - Backup → letztes Backup → Zurückspielen
Verwandte Dokus
webserver-doku.html— DNS-Updates, ipv64, NPM, System-Updatesstunden-doku.html— Stundenabrechnung-App auf gleicher VMtreckertreff-doku.html— Tretbeckenreinigungs-Team auf gleicher VManleitung.html(im Abo-Paket) — Original-Installations-Anleitung
Abo-Verwaltung Admin-Doku Stand: 22.05.2026
https://abos.rusti.ipv64.net auf VM 122-g50zeiten