Unifi-Voucher-Tool/tools/demo/build.py
Friederich Loheide 4159b92268
Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
Display-Seiten individuell gestalten
Jede Display-Seite bringt jetzt ihr eigenes Erscheinungsbild mit – der
Empfang sieht anders aus als der Tagungsraum nebenan:

- eigenes Logo (leer = Logo aus den Einstellungen)
- formatfüllendes Hintergrundbild mit einstellbarer Abdunklung (0–90 %),
  damit die Karte auf hellen Fotos lesbar bleibt
- eigene Akzentfarbe für den Knopf (leer = Farbe aus dem Design-Tab)
- Karte wahlweise hell oder dunkel; auf Fotos wirkt dunkel meist ruhiger

Logo und Hintergrund lassen sich hochladen oder als URL hinterlegen; beim
Löschen einer Display-Seite verschwinden die hochgeladenen Dateien mit.

Nebenbei aufgeräumt: das Bildfeld (Vorschau + Upload + URL + Entfernen)
lag als Funktion in admin/settings.php und wird jetzt von beiden Seiten
genutzt – Ui::imageField() für die Darstellung, Upload::resolveField()
für die Auswertung.

Sicherheit: die Akzentfarbe landet in einem style-Attribut, deshalb wird
sie sowohl beim Speichern als auch beim Ausgeben auf eine echte Hex-Farbe
geprüft; ein Test hält das fest.

Migration 0006, database.sql nachgezogen, 4 neue Tests (42 gesamt),
Screenshots ergänzt, Version 2.8.0.

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

123 lines
4.9 KiB
Python
Executable file
Raw Permalink 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(")
# Kiosk: ausgegebenen Code zeigen, ohne echten Controller (?demo=code)
patch(os.path.join(target, 'kiosk.php'),
"$headline = trim((string)($kiosk['headline'] ?? ''))",
'''if (($_GET['demo'] ?? '') === 'code' && $kiosk) {
$voucher = ['code' => '4829-17364', 'site_name' => $kiosk['site_name'], 'max_uses' => 2,
'expire_min' => 480, 'expiry_date' => '24.09.2026', 'expiry_time' => '08:00'];
}
$headline = trim((string)($kiosk['headline'] ?? ''))''')
# Kiosk-Verwaltung: Formular fuer den Screenshot geoeffnet zeigen
patch(os.path.join(target, 'admin', 'kiosks.php'),
'<div class="modal" id="kioskModal">',
'<div class="modal<?= ($_GET[\'demo\'] ?? \'\') === \'form\' ? \' active\' : \'\' ?>" id="kioskModal">')
# 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())