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>
This commit is contained in:
parent
ba90ffef03
commit
6da46f040a
57 changed files with 1479 additions and 267 deletions
31
tools/demo/overlay/includes/Auth.php
Normal file
31
tools/demo/overlay/includes/Auth.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/** Demo-Stub: angemeldeter Admin ohne echte Sitzungspruefung. */
|
||||
require_once __DIR__ . '/Database.php';
|
||||
require_once __DIR__ . '/Totp.php';
|
||||
class Auth {
|
||||
private $user = ['id'=>1,'name'=>'Marie Sander','email'=>'marie.sander@example.com','is_admin'=>1,
|
||||
'role'=>'admin','is_active'=>1,'password_hash'=>'x','totp_enabled'=>0,'auth_source'=>'local'];
|
||||
public function __construct() { if (session_status() === PHP_SESSION_NONE) @session_start(); $_SESSION['user_id'] = 1; }
|
||||
public function clientIp() { return '192.168.10.42'; }
|
||||
public function login($e, $p) { return false; }
|
||||
public function isTotpPending() { return isset($_GET['totp']); }
|
||||
public function verifyTotpLogin($c) { return false; }
|
||||
public function enableTotp($u, $s) { return true; }
|
||||
public function disableTotp($u) { return true; }
|
||||
public function generateBackupCodes($n = 8) { return ['A1B2-C3D4','E5F6-G7H8','J9K0-L1M2','N3P4-Q5R6']; }
|
||||
public function regenerateBackupCodes($u) { return $this->generateBackupCodes(); }
|
||||
public function backupCodesRemaining($u) { return 6; }
|
||||
public function writeAuditLog(...$a) { return true; }
|
||||
public function logout() {}
|
||||
public function isLoggedIn() { return !isset($_GET['anon']); }
|
||||
public function isAdmin() { return $this->isLoggedIn(); }
|
||||
public function getCurrentUser() { return $this->isLoggedIn() ? $this->user : null; }
|
||||
public function hasAccessToSite($id) { return true; }
|
||||
public function validateCsrfToken($t) { return true; }
|
||||
public function getCsrfToken() { return 'demo-csrf-token'; }
|
||||
public function registerUser(...$a) { return true; }
|
||||
public function requireAdmin() {}
|
||||
public function requireLogin() {}
|
||||
public function activeSessionCount() { return 3; }
|
||||
public function logoutOtherSessions() { return 2; }
|
||||
}
|
||||
164
tools/demo/overlay/includes/Database.php
Normal file
164
tools/demo/overlay/includes/Database.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
/** Demo-Stub: liefert feste Beispieldaten statt echter DB-Zugriffe. */
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $settings = [
|
||||
'app_title' => 'UniFi Voucher System',
|
||||
'logo_url' => '', 'favicon_url' => '',
|
||||
'public_access' => '1', 'smtp_enabled' => '1', 'sms_enabled' => '1',
|
||||
'm365_enabled' => '1', 'oidc_enabled' => '0', 'oidc_name' => 'Keycloak',
|
||||
'default_expire_minutes' => '480', 'default_max_uses' => '2', 'max_uses_limit' => '10',
|
||||
'instruction_header' => 'So verbinden Sie sich',
|
||||
'instruction_text' => 'WLAN „Gast-WiFi" wählen, Code eingeben und bestätigen. Bei Fragen hilft der Empfang gerne weiter.',
|
||||
'last_cron_sync' => '2026-09-22 14:35:00',
|
||||
'session_driver' => 'db', 'captcha_mode' => 'math',
|
||||
'enforce_2fa_admins' => '1', 'user_daily_voucher_limit' => '25',
|
||||
'webhook_enabled' => '1', 'webhook_url' => 'https://hooks.slack.com/services/T024/B0197/XYZ',
|
||||
'twilio_sid' => 'AC8f2c1d94e77b40a2', 'twilio_from' => '+4915112345678',
|
||||
'trusted_proxy' => '10.0.0.1, 172.18.0.1',
|
||||
'cleanup_expired_days' => '90', 'cleanup_audit_days' => '365', 'cleanup_login_days' => '30',
|
||||
'last_cleanup' => '2026-09-22 03:00:00',
|
||||
'print_template' => '<div style="padding:20px;border:1px dashed #999;"><h2>{APP_TITLE}</h2><p>{SITE_NAME}</p><h1>{VOUCHER_CODE}</h1></div>',
|
||||
];
|
||||
public function __construct()
|
||||
{
|
||||
// Screenshot-Varianten: ?brand=custom|gradient|nopanel
|
||||
$brand = $_GET['brand'] ?? '';
|
||||
if ($brand === 'custom') {
|
||||
$this->settings = array_merge($this->settings, [
|
||||
'app_title' => 'Hotel Seeblick',
|
||||
'login_brand_name' => 'Hotel Seeblick',
|
||||
'login_logo_url' => '/demo-assets/logo.svg',
|
||||
'login_claim_title' => 'Willkommen im Hotel Seeblick.',
|
||||
'login_claim_text' => 'Gäste-WLAN für Zimmer, Tagungsräume und Restaurant – Zugangscodes direkt an der Rezeption erstellen.',
|
||||
'login_features' => "Code direkt beim Check-in ausdrucken\nTagungsgäste per E-Mail versorgen\nAuswertung je Haus und Etage",
|
||||
'login_footer' => '© 2026 Hotel Seeblick GmbH · Datenschutz · Impressum',
|
||||
'login_bg_image' => '/demo-assets/background.svg',
|
||||
'login_bg_overlay' => '45',
|
||||
'brand_accent' => '#0f766e',
|
||||
'brand_accent_dark' => '#2dd4bf',
|
||||
'brand_gradient_from' => '#0f766e',
|
||||
'brand_gradient_to' => '#0ea5e9',
|
||||
]);
|
||||
} elseif ($brand === 'nopanel') {
|
||||
$this->settings = array_merge($this->settings, [
|
||||
'login_panel_enabled' => '0',
|
||||
'login_brand_name' => 'Stadtwerke Nordheim',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance() { return self::$instance ??= new self(); }
|
||||
public function getConnection() { return null; }
|
||||
public function getSetting($key, $default = null) { return $this->settings[$key] ?? $default; }
|
||||
public function setSetting($key, $value) { $this->settings[$key] = $value; }
|
||||
public function execute($sql, $params = []) { return 1; }
|
||||
public function query($sql, $params = []) { return new DemoStmt(); }
|
||||
public function fetchOne($sql, $params = []) { $r = $this->fetchAll($sql, $params); return $r[0] ?? false; }
|
||||
|
||||
public function fetchAll($sql, $params = []) {
|
||||
$s = preg_replace('/\s+/', ' ', strtolower($sql));
|
||||
|
||||
if (str_contains($s, 'count(*) as count from sites')) return [['count'=>4]];
|
||||
if (str_contains($s, 'count(*) as count from users')) return [['count'=>12]];
|
||||
if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=curdate()')) return [['count'=>18]];
|
||||
if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=')) {
|
||||
$day = $params[0] ?? '';
|
||||
$map = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
|
||||
$idx = abs((int)((strtotime($day) - strtotime('today')) / 86400));
|
||||
return [['count' => $map[13 - min($idx, 13)]]];
|
||||
}
|
||||
if (str_contains($s, 'from vouchers where site_id=?') && str_contains($s, 'count(*) as total')) {
|
||||
$per = [1=>['total'=>128,'valid'=>74,'used'=>39,'expired'=>15],
|
||||
2=>['total'=>63,'valid'=>41,'used'=>18,'expired'=>4],
|
||||
3=>['total'=>22,'valid'=>14,'used'=>6,'expired'=>2],
|
||||
4=>['total'=>0,'valid'=>0,'used'=>0,'expired'=>0]];
|
||||
return [$per[(int)($params[0] ?? 1)] ?? $per[4]];
|
||||
}
|
||||
if (str_contains($s, 'count(*) as total') && str_contains($s, 'from vouchers')) {
|
||||
return [['total'=>213,'valid'=>129,'used'=>63,'expired'=>21]];
|
||||
}
|
||||
if (str_contains($s, 'from sites')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Hauptstandort Nord','site_id'=>'default','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-01-12 09:00:00'],
|
||||
['id'=>2,'name'=>'Campus West','site_id'=>'campus-west','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-02-03 11:30:00'],
|
||||
['id'=>3,'name'=>'Showroom Berlin','site_id'=>'showroom','unifi_controller_url'=>'https://unifi-b.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-04-21 15:10:00'],
|
||||
['id'=>4,'name'=>'Lager Süd','site_id'=>'lager','unifi_controller_url'=>'https://unifi-s.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>0,'created_at'=>'2026-06-08 08:45:00'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'count(v.id) as voucher_count') || str_contains($s, 'from users u left join vouchers')) {
|
||||
return [
|
||||
['name'=>'Marie Sander','email'=>'marie.sander@example.com','voucher_count'=>48],
|
||||
['name'=>'Jonas Weber','email'=>'j.weber@example.com','voucher_count'=>31],
|
||||
['name'=>'Alina Brandt','email'=>'alina.brandt@example.com','voucher_count'=>27],
|
||||
['name'=>'Tim Kurz','email'=>'tim.kurz@example.com','voucher_count'=>19],
|
||||
['name'=>'Sara Vogt','email'=>'sara.vogt@example.com','voucher_count'=>12],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from vouchers')) {
|
||||
if (str_contains($s, 'group by date')) {
|
||||
$out = []; $vals = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
|
||||
foreach ($vals as $i => $c) { $out[] = ['date'=>date('d.m', strtotime('-'.(13-$i).' days')), 'count'=>$c]; }
|
||||
return $out;
|
||||
}
|
||||
$codes = [['H7K2-M9QF',1,'Hauptstandort Nord','valid'],['P3RT-8ZXC',2,'Campus West','used'],
|
||||
['QW4E-7TYU',1,'Hauptstandort Nord','valid'],['LM2N-5BVD',3,'Showroom Berlin','expired'],
|
||||
['ZX9C-3PLO',2,'Campus West','valid'],['FG6H-1JKL',1,'Hauptstandort Nord','used']];
|
||||
$names = ['Besuch Agentur Nordlicht','Workshop Raum 2','Empfang Tagesgast','Messe-Stand','Handwerker Haustechnik','Bewerbungsgespräch'];
|
||||
$out = [];
|
||||
foreach ($codes as $i => [$c,$sid,$sname,$st]) {
|
||||
$out[] = ['id'=>$i+1,'voucher_code'=>$c,'site_id'=>$sid,'site_name'=>$sname,'user_name'=>'Marie Sander',
|
||||
'status'=>$st,'created_at'=>date('Y-m-d H:i:s', strtotime("-{$i} hours")),
|
||||
'voucher_name'=>$names[$i],'unifi_voucher_id'=>'65f1a'.$i,
|
||||
'expires_at'=>date('Y-m-d H:i:s', strtotime('+'.(8-$i).' hours')),
|
||||
'max_uses'=>[2,5,1,3,2,1][$i],'used_count'=>[2,1,0,3,0,1][$i],'expire_minutes'=>480];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (str_contains($s, 'from users')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Marie Sander','email'=>'marie.sander@example.com','is_admin'=>1,'is_active'=>1,'created_at'=>'2026-01-10 09:00:00','last_login'=>'2026-09-22 08:12:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>48,'password_hash'=>'x'],
|
||||
['id'=>2,'name'=>'Jonas Weber','email'=>'j.weber@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-03-02 13:20:00','last_login'=>'2026-09-21 17:40:00','totp_enabled'=>0,'auth_source'=>'m365','voucher_count'=>31,'password_hash'=>''],
|
||||
['id'=>3,'name'=>'Alina Brandt','email'=>'alina.brandt@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-05-18 10:05:00','last_login'=>'2026-09-20 09:31:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>27,'password_hash'=>'x'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from api_keys')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Buchungssystem','key_prefix'=>'3f9a2c','scope'=>'write','rate_limit'=>60,'is_active'=>1,'last_used_at'=>'2026-09-22 09:12:00','creator'=>'Marie Sander','created_at'=>'2026-06-01 10:00:00'],
|
||||
['id'=>2,'name'=>'Terminal Foyer','key_prefix'=>'b71e04','scope'=>'read','rate_limit'=>0,'is_active'=>1,'last_used_at'=>'2026-09-21 17:40:00','creator'=>'Marie Sander','created_at'=>'2026-07-14 16:20:00'],
|
||||
['id'=>3,'name'=>'Altes Kassensystem','key_prefix'=>'c0d5f8','scope'=>'write','rate_limit'=>30,'is_active'=>0,'last_used_at'=>null,'creator'=>'Jonas Weber','created_at'=>'2026-02-09 08:05:00'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from audit_log')) {
|
||||
if (str_contains($s, 'count(')) return [['total'=>1284,'c'=>1284,'count'=>1284]];
|
||||
if (str_contains($s, 'distinct action')) {
|
||||
return [['action'=>'voucher_created'],['action'=>'user_login'],['action'=>'settings_saved'],['action'=>'user_created'],['action'=>'template_updated']];
|
||||
}
|
||||
$rows = [['2026-09-22 14:32:00','Marie Sander','voucher_created','Hauptstandort Nord · H7K2-M9QF','192.168.10.42'],
|
||||
['2026-09-22 13:58:00','Jonas Weber','user_login','Microsoft 365','83.112.4.17'],
|
||||
['2026-09-22 11:04:00','Marie Sander','settings_saved','Tab: E-Mail (SMTP)','192.168.10.42'],
|
||||
['2026-09-21 17:40:00','Alina Brandt','user_created','tim.kurz@example.com','192.168.10.88'],
|
||||
['2026-09-21 09:12:00','Marie Sander','template_updated','Tagesgast','192.168.10.42'],
|
||||
['2026-09-20 16:03:00','Jonas Weber','voucher_bulk','5 Vouchers · Campus West','83.112.4.17']];
|
||||
$out = [];
|
||||
foreach ($rows as $i => [$d,$u,$a,$det,$ip]) {
|
||||
$out[] = ['id'=>$i+1,'created_at'=>$d,'user_name'=>$u,'user_email'=>'demo@example.com','action'=>$a,'details'=>$det,'ip_address'=>$ip,'entity_type'=>'voucher','entity_id'=>$i+1];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (str_contains($s, 'templates')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Tagesgast','description'=>'Standardprofil für Besucher am Empfang','max_uses'=>2,'expire_minutes'=>480,'qos_rate_max_down'=>20000,'qos_rate_max_up'=>5000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>1,'created_at'=>'2026-01-15 10:00:00'],
|
||||
['id'=>2,'name'=>'Konferenz','description'=>'Mehrtägige Veranstaltungen','max_uses'=>5,'expire_minutes'=>4320,'qos_rate_max_down'=>50000,'qos_rate_max_up'=>10000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-02-20 11:30:00'],
|
||||
['id'=>3,'name'=>'Handwerker','description'=>'Kurzzugang für Dienstleister','max_uses'=>1,'expire_minutes'=>240,'qos_rate_max_down'=>10000,'qos_rate_max_up'=>2000,'qos_usage_quota'=>1024,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-03-08 09:15:00'],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
class DemoStmt {
|
||||
public function fetch($m = null) { return false; }
|
||||
public function fetchAll($m = null) { return []; }
|
||||
public function rowCount() { return 1; }
|
||||
public function execute($p = []) { return true; }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue