Display-Seiten: Gäste holen sich den Zugang selbst

Für Empfang, Lobby oder Tagungsraum lässt sich je Site eine öffentliche
Seite anlegen (kiosk.php), die auf einem Bildschirm oder Tablet läuft:
ein großer Knopf, ein Klick, ein Zugangscode mit QR-Code. Nach einer
einstellbaren Anzeigedauer springt der Bildschirm zurück, damit der
nächste Gast nicht den Code seines Vorgängers sieht.

Verwaltung unter Administration → Display-Seiten:
- Site und optionales Voucher-Profil (bestimmt Laufzeit, Geräte, QoS)
- eigene Überschrift und Text für den Bildschirm
- Codes pro Tag, Wartezeit zwischen zwei Codes, Anzeigedauer
- geheimer Link zum Kopieren, als QR-Code anzeigbar und jederzeit
  erneuerbar (der alte Link gilt dann sofort nicht mehr)

Absicherung: der Link ist der Zugang, deshalb Tageslimit und Wartezeit
je Display, CSRF-Token am Formular, `noindex` im Kopf und ein Eintrag im
Audit-Log für jeden ausgegebenen Code. Webhooks werden für Kiosk-Codes
bewusst nicht ausgelöst – ein Empfangsdisplay würde den Kanal fluten.

Technik:
- neue Tabelle `kiosks`, `vouchers.kiosk_id` hält die Herkunft fest
  (Migration 0005, database.sql nachgezogen)
- includes/Kiosk.php kapselt Token, Limits und Profil-Auflösung
- includes/VoucherService.php bündelt die Voucher-Erstellung, die vorher
  in index.php lag und für den Kiosk ein zweites Mal nötig gewesen wäre
- Startbildschirm zeigt zusätzlich einen QR auf sich selbst, damit Gäste
  die Seite am eigenen Handy öffnen können

Tests: 9 neue Fälle für Token-Prüfung, Wartezeit, Tageslimit und
Profil-Auflösung (38 Tests gesamt), PHPStan deckt Kiosk.php mit ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 16:51:50 +00:00
parent 5d72febadc
commit 61810eb050
30 changed files with 1360 additions and 29 deletions

View file

@ -470,6 +470,59 @@ return [
'print_valid_until' => 'Valid until',
'print_devices' => 'devices',
'credit_by' => 'Developed by',
'kiosk_default_headline' => 'Free guest Wi-Fi',
'kiosk_default_subline' => 'Tap the button you will get your personal access code right away.',
'kiosk_button' => 'Get access code',
'kiosk_working' => 'One moment…',
'kiosk_ready' => 'Your access code',
'kiosk_scan_code' => 'Scan the QR code or type the code',
'kiosk_reset_in' => 'This screen resets in {seconds} seconds.',
'kiosk_done' => 'Done',
'kiosk_phone_hint' => 'Or scan with your phone and open it there',
'kiosk_error_cooldown' => 'A code was just issued. Please wait {seconds} seconds.',
'kiosk_error_limit' => 'No more access codes available today. Please ask at the reception desk.',
'kiosk_error_generic' => 'The access code could not be created. Please try again.',
'kiosk_unknown' => 'This page is not available',
'kiosk_unknown_hint' => 'The link is invalid or has been deactivated.',
'audit_action_voucher_kiosk' => 'Voucher taken at display',
'nav_kiosks' => 'Display pages',
'kiosks_title' => 'Display pages',
'kiosks_subtitle' => 'Public pages for screens and tablets guests get their access themselves.',
'kiosks_add' => 'Add display page',
'kiosks_edit' => 'Edit display page',
'kiosks_empty' => 'No display page created yet.',
'kiosks_no_sites' => 'Create a site first, then you can add a display page for it.',
'kiosks_name' => 'Label',
'kiosks_name_placeholder' => 'e.g. reception ground floor',
'kiosks_name_hint' => 'Appears in the audit log and as the voucher name.',
'kiosks_template' => 'Voucher profile',
'kiosks_no_template' => 'Use default values',
'kiosks_template_hint' => 'Defines duration, device count and bandwidth of the codes issued.',
'kiosks_headline' => 'Headline on screen',
'kiosks_subline' => 'Text below',
'kiosks_daily_limit' => 'Codes per day',
'kiosks_daily_limit_hint' => '0 = unlimited. Protects against misuse if the link gets shared.',
'kiosks_cooldown' => 'Cooldown (seconds)',
'kiosks_cooldown_hint' => 'Delay between two codes on this display.',
'kiosks_display' => 'Display duration (seconds)',
'kiosks_display_hint' => 'After that the screen returns to the start screen.',
'kiosks_active' => 'Display page active',
'kiosks_link' => 'Public link',
'kiosks_open' => 'Open',
'kiosks_qr' => 'QR code',
'kiosks_qr_hint' => 'Put it up next to the screen or photograph it to open the page on a tablet.',
'kiosks_today' => 'today',
'kiosks_total' => 'in total',
'kiosks_renew' => 'Renew link',
'kiosks_renew_confirm' => 'Generate a new link? The previous link will stop working.',
'kiosks_delete_confirm' => 'Really delete this display page?',
'kiosks_added' => 'Display page created.',
'kiosks_updated' => 'Display page saved.',
'kiosks_deleted' => 'Display page deleted.',
'kiosks_token_renewed' => 'New link generated the old one is no longer valid.',
'audit_action_kiosk_created' => 'Display page created',
'audit_action_kiosk_updated' => 'Display page updated',
'audit_action_kiosk_deleted' => 'Display page deleted',
'settings_tab_general' => 'General',
'settings_tab_defaults' => 'Voucher Defaults',
'settings_tab_cron' => 'Cron Sync',