Unifi-Voucher-Tool/tools/demo/build.py
Friederich Loheide 6da46f040a Alle Frontend-Assets lokal ausliefern statt über CDNs
Inter, Font Awesome, Chart.js, qrcodejs und TinyMCE liegen jetzt unter
assets/vendor/ und werden vom eigenen Server ausgeliefert.

Warum:
- Datenschutz: bisher ging bei jedem Seitenaufruf die IP der Nutzer an
  Google Fonts, cdnjs, jsDelivr und Tiny Cloud
- Funktion: UniFi-Installationen stehen oft in abgeschotteten Netzen –
  dort fehlten bisher Schrift, Icons, Diagramme und Editor

Neu: includes/Ui.php
- Ui::head()/Ui::script() binden die Assets ein und hängen einen
  Versionsstempel an (?v=filemtime), damit Browser nach einem Update
  nicht das alte CSS aus dem Cache nehmen
- Ui::themeScript() setzt das Theme aus der gespeicherten Auswahl oder
  – wenn keine vorliegt – aus prefers-color-scheme; global.js folgt
  Systemwechseln live, solange nichts manuell gewählt wurde
- <meta name="color-scheme"> ergänzt, damit Formularelemente passen

Der TinyMCE-API-Key entfällt: der Editor läuft immer lokal (GPL-Variante),
inklusive deutscher Oberfläche, wenn die App auf Deutsch steht.

Neu: tools/demo/build.py – baut aus dem Projekt eine Demo-Instanz ohne
Datenbank (Stubs für Database/Auth, feste Beispieldaten). Damit lassen
sich Screenshots reproduzierbar erzeugen und alle Seiten einmal rendern
(Smoke-Test), ohne eine MySQL-Instanz aufzusetzen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 06:23:44 +00:00

108 lines
4.2 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Baut eine Demo-Instanz des Voucher-Tools ohne Datenbank.
Die Demo dient zwei Zwecken:
* Screenshots fuer die Dokumentation reproduzierbar erzeugen
* Smoke-Test: rendert jede Seite einmal ohne echte Datenbank
Ablauf: Projekt in ein Zielverzeichnis kopieren, das Overlay darueberlegen
(Stubs fuer Database/Auth) und ein paar klar benannte Demo-Zustaende in die
Kopie patchen. Das Original bleibt unveraendert.
python3 tools/demo/build.py /tmp/uvt-demo
php -S 127.0.0.1:8123 -t /tmp/uvt-demo
"""
import os
import shutil
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OVERLAY = os.path.join(ROOT, 'tools', 'demo', 'overlay')
# Nur auf oberster Ebene ueberspringen assets/vendor gehoert dazu!
SKIP_TOP = {'.git', '.github', 'tools', 'docs', 'tests', 'vendor', 'node_modules'}
def copy_project(target: str) -> None:
if os.path.exists(target):
shutil.rmtree(target)
def ignore(directory, names):
if os.path.abspath(directory) == ROOT:
return [n for n in names if n in SKIP_TOP]
return []
shutil.copytree(ROOT, target, ignore=ignore)
for name in os.listdir(OVERLAY):
src = os.path.join(OVERLAY, name)
dst = os.path.join(target, name)
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
def patch(path: str, anchor: str, replacement: str) -> None:
"""Ersetzt einen Anker in der Demo-Kopie. Fehlt der Anker, bricht der
Build ab so faellt auf, wenn sich die Vorlage geaendert hat."""
with open(path, encoding='utf-8') as fh:
content = fh.read()
if anchor not in content:
raise SystemExit('Anker nicht gefunden in %s:\n%s' % (path, anchor[:80]))
with open(path, 'w', encoding='utf-8') as fh:
fh.write(content.replace(anchor, replacement, 1))
def apply_demo_states(target: str) -> None:
# Voucher-Ergebnis und Bulk-Ansicht ohne Controller-Zugriff zeigen
anchor = "$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;"
patch(os.path.join(target, 'index.php'), anchor, anchor + """
// --- Demo-Instanz: Zustaende ohne UniFi-Controller darstellen ---
if (($_GET['demo'] ?? '') === 'result') {
$voucherCreated = true;
$voucherCode = '4829-17364';
$voucherData = ['code' => '4829-17364', 'site_name' => 'Hauptstandort Nord', 'max_uses' => 2,
'expire_min' => 480, 'expiry_date' => '23.09.2026', 'expiry_time' => '08:00'];
}
if (($_GET['demo'] ?? '') === 'bulk') {
$bulkCreated = true;
$bulkVouchers = [];
foreach (['4829-17364', '5517-90422', '7731-64508', '2094-38177', '6640-52913'] as $code) {
$bulkVouchers[] = ['code' => $code, 'site_name' => 'Hauptstandort Nord', 'max_uses' => 2,
'expire_min' => 480, 'expiry_date' => '23.09.2026', 'expiry_time' => '08:00'];
}
}
""")
# Live-Voucher-Liste automatisch laden (sonst wartet sie auf Site-Auswahl)
patch(os.path.join(target, 'admin', 'vouchers.php'), "</script>\n</body>", """</script>
<script>
// Demo-Instanz: Site vorauswaehlen und Liste laden
document.addEventListener('DOMContentLoaded', function () {
const sel = document.getElementById('siteSelect');
if (sel) { sel.value = '1'; loadVouchers(false); }
});
</script>
</body>""")
# Frisch erzeugten API-Schluessel zeigen
patch(os.path.join(target, 'admin', 'api_keys.php'), '$keys = $db->fetchAll(',
"if (($_GET['demo'] ?? '') === 'new') { $newKey = 'uvt_3f9a2c7d41e8b60592af18cc4d7e0b3a95f2617c'; }\n$keys = $db->fetchAll(")
# Theme per Query-Parameter erzwingen (fuer Dark-Mode-Screenshots)
ui = os.path.join(target, 'includes', 'Ui.php')
patch(ui, 'var s=localStorage.getItem("theme");',
'var s=new URLSearchParams(location.search).get("theme")||localStorage.getItem("theme");')
def main() -> int:
target = sys.argv[1] if len(sys.argv) > 1 else '/tmp/uvt-demo'
copy_project(target)
apply_demo_states(target)
print('Demo-Instanz gebaut: %s' % target)
print('Start: php -S 127.0.0.1:8123 -t %s' % target)
return 0
if __name__ == '__main__':
raise SystemExit(main())