diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..2654739 --- /dev/null +++ b/.htaccess @@ -0,0 +1,26 @@ +# --------------------------------------------------------------------------- +# Sicherheits-Header und Zugriffsschutz (Apache) +# Nginx-Entsprechung siehe Readme.md, Abschnitt "Sicherheit". +# --------------------------------------------------------------------------- + + + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" + + # Alle Frontend-Assets liegen lokal; externe Quellen nur fuer hCaptcha, + # falls es in den Einstellungen aktiviert wurde. + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'unsafe-inline' https://*.hcaptcha.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://*.hcaptcha.com; frame-src https://*.hcaptcha.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" + + +# Kein Verzeichnislisting +Options -Indexes + +# Dateien, die nie direkt ausgeliefert werden sollen + + Require all denied + + +# Interne Ordner schuetzen sich ueber eigene .htaccess-Dateien +# (funktioniert auch bei Installation in einem Unterverzeichnis). diff --git a/Dockerfile b/Dockerfile index 3d0f28c..9696aa2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,11 +16,14 @@ RUN { \ echo 'post_max_size=8M'; \ } > /usr/local/etc/php/conf.d/zz-voucher.ini +# .htaccess auswerten (Sicherheits-Header, Schutz des uploads-Ordners) +RUN sed -ri 's!!\n\tAllowOverride All!g' /etc/apache2/apache2.conf + WORKDIR /var/www/html COPY . /var/www/html -# Laufzeit-Verzeichnis des Updaters beschreibbar machen -RUN mkdir -p /var/www/html/updater/storage \ +# Laufzeit-Verzeichnisse beschreibbar machen +RUN mkdir -p /var/www/html/updater/storage /var/www/html/uploads \ && chown -R www-data:www-data /var/www/html COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/Readme.md b/Readme.md index af6cb97..e558be6 100644 --- a/Readme.md +++ b/Readme.md @@ -8,7 +8,7 @@ ![MySQL](https://img.shields.io/badge/MySQL-5.7%2B%20%2F%20MariaDB-4479A1?logo=mysql&logoColor=white) ![UniFi OS](https://img.shields.io/badge/UniFi%20OS-7.0%2B-0559C9?logo=ubiquiti&logoColor=white) ![License](https://img.shields.io/badge/Lizenz-MIT-green) -![Version](https://img.shields.io/badge/Version-2.5.0-blueviolet) +![Version](https://img.shields.io/badge/Version-2.6.0-blueviolet) ![CI](https://github.com/friloo/unifi-voucher-tool/actions/workflows/ci.yml/badge.svg) @@ -47,6 +47,11 @@ - 🌍 **Öffentlicher Modus** – optional ohne Login nutzbar (mit CSRF-Schutz & Throttle) - 🎹 **Einheitliches Design-System** – ein Stylesheet fĂŒr Frontend, Login und Backend (Tokens, Komponenten, Light/Dark) - đŸ·ïž **Login-Seite individualisierbar** – Firmenname, Logo, Texte, Hintergrundbild bzw. Farbverlauf +- đŸ–Œïž **Eigene Markenfarben** – Akzentfarbe, Verlauf und Eckenradius wirken auf die gesamte OberflĂ€che +- âŹ†ïž **Bild-Upload** fĂŒr Logo, Favicon und Login-Hintergrund (kein externes Hosting nötig) +- 🔒 **Keine externen CDNs** – Schrift, Icons, Diagramme und Editor werden lokal ausgeliefert (DSGVO, Offline-Netze) +- ♿ **Barrierearm** – Kontraste nach WCAG AA, Sprungmarke, aria-Beschriftungen, `prefers-reduced-motion` +- đŸ“± **Mobil nutzbar** – Tabellen werden auf schmalen GerĂ€ten zu Karten - 🌗 **Dark Mode** – umschaltbar, Einstellung wird im Browser gespeichert - 🌐 **Mehrsprachig** – Deutsch / Englisch per Umschalter (`lang/`) - đŸ“± **Responsive Admin-Layout** mit Hamburger-MenĂŒ & Sidebar-Overlay @@ -92,6 +97,11 @@ Einstellungen +
+ Markenfarben einstellen + Ansicht auf dem Smartphone +
+ ### REST-API, 2FA & Integrationen
@@ -249,21 +259,44 @@ gemeinsames Stylesheet: **`assets/global.css`**. - **Dark Mode** ausschließlich ĂŒber Tokens – keine `!important`-Overrides mehr - **Schriftart** Inter (via Google Fonts) mit System-Font-Fallback -Eigenes Branding lĂ€sst sich meist mit wenigen Zeilen umsetzen – z. B. in einer -eigenen CSS-Datei oder direkt in `assets/global.css`: +### Markenfarben ohne Code + +Unter **Administration → Einstellungen → Design** lassen sich Akzentfarbe +(hell und dunkel), Markenverlauf und Eckenradius setzen. Abgeleitete Töne – +Hover, weiche FlĂ€chen, Rahmen, Fokusring – berechnet das System per `color-mix` +aus der Grundfarbe; eine Farbe genĂŒgt also. Eine Live-Vorschau zeigt Button, +Badge, Chip und Logo-Kachel sofort im neuen Ton. + +Die Werte landen als schlanker `:root`-Override im Seitenkopf und gelten ĂŒberall, +auch auf Login-Seite, Installer und Updater. Wer lieber in CSS arbeitet, kann +dieselben Variablen weiterhin in `assets/global.css` ĂŒberschreiben: ```css :root { --accent: #0f766e; /* PrimĂ€rfarbe (Buttons, aktive Navigation) */ - --accent-hover: #0d5f59; - --accent-soft: #e6f4f2; /* FlĂ€chen fĂŒr aktive ZustĂ€nde */ --brand-gradient: linear-gradient(135deg, #0f766e 0%, #0ea5e9 100%); --r-lg: 14px; /* Eckenradius fĂŒr Cards */ } ``` -Logo und Favicon werden nicht ĂŒber CSS, sondern unter -**Administration → Einstellungen → Allgemein** gesetzt. +### Bilder hochladen + +Logo, Favicon, Login-Logo und Login-Hintergrund lassen sich direkt hochladen – +alternativ bleibt das URL-Feld bestehen. Die Dateien landen unter `uploads/` +(Docker: eigenes Volume, siehe unten). Erlaubt sind PNG, JPG, WEBP, GIF und SVG +bis 3 MB; SVGs werden vor dem Speichern von Skripten und externen Verweisen +befreit, und im Upload-Ordner sperrt eine `.htaccess` die PHP-AusfĂŒhrung. + +### Assets ohne Drittanbieter + +Schrift (Inter), Icons (Font Awesome), Diagramme (Chart.js), QR-Codes und der +WYSIWYG-Editor (TinyMCE) liegen unter `assets/vendor/` und kommen vom eigenen +Server. Das hĂ€lt Besucher-IPs bei Ihnen – und die OberflĂ€che funktioniert auch +dort, wo das Netz keinen Weg nach außen hat. Details und Aktualisierungs-Hinweise: +[`assets/vendor/README.md`](assets/vendor/README.md). + +Alle Asset-URLs tragen einen Versionsstempel (`?v=
`), damit Browser nach einem +Update nicht die alten Dateien aus dem Cache verwenden. ### Login-Seite individualisieren @@ -303,13 +336,30 @@ Das Tool ist auf einen sicheren Standardbetrieb ausgelegt: | **Sessions** | HttpOnly, SameSite, strict mode + absolutes Timeout | | **Fehler** | `display_errors` aus, `log_errors` an (kein Info-Leak) | -Empfohlene zusĂ€tzliche HĂ€rtung am Server: +Mitgeliefert wird eine `.htaccess` im Projektstamm mit Sicherheits-Headern +(`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, +`Permissions-Policy` und einer Content-Security-Policy). Da alle Assets lokal +liegen, erlaubt die CSP nur noch die eigene Herkunft – externe Verbindungen +bleiben lediglich fĂŒr hCaptcha offen, falls es aktiviert wird. Ordner wie +`includes/`, `tools/`, `tests/` und `uploads/` schĂŒtzen sich ĂŒber eigene +`.htaccess`-Dateien. -```apache -# .htaccess – sensible Dateien sperren (wird vom Installer erzeugt) - - Require all denied - +> **Apache:** `AllowOverride All` muss fĂŒr das Verzeichnis gesetzt sein, sonst +> werden die `.htaccess`-Dateien ignoriert. Das mitgelieferte Docker-Image +> erledigt das bereits. + +FĂŒr **Nginx** entspricht das: + +```nginx +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self'" always; + +location ~ ^/(includes|tools|tests)/ { deny all; } +location ~ ^/updater/(storage|migrations)/ { deny all; } +location ~ ^/(config\.php|database\.sql)$ { deny all; } +location ^~ /uploads/ { location ~ \.php$ { deny all; } } ``` ```sql @@ -432,6 +482,40 @@ Das Schema wird beim ersten Start automatisch in MariaDB geladen; danach den Installer (`/install.php`) fĂŒr den Admin-Account aufrufen oder Config per ENV setzen (`DB_*`, `APP_KEY`). +Hochgeladene Logos und HintergrĂŒnde liegen im Volume `uploads` und ĂŒberstehen +damit ein Image-Update. Bei eigener Apache-/Nginx-Installation muss `uploads/` +fĂŒr den Webserver beschreibbar sein: + +```bash +chown -R www-data:www-data uploads updater/storage +``` + +--- + +## đŸ§Ș Entwicklung + +FĂŒr Screenshots und einen schnellen Durchlauf aller Seiten gibt es eine +Demo-Instanz **ohne Datenbank** – `Database` und `Auth` werden durch Stubs mit +festen Beispieldaten ersetzt: + +```bash +python3 tools/demo/build.py /tmp/uvt-demo +php -S 127.0.0.1:8123 -t /tmp/uvt-demo & + +# Bilder in docs/screenshots neu erzeugen (benötigt headless Chromium) +CHROME_BIN=/usr/bin/chromium python3 tools/screenshots.py +``` + +Details und die verfĂŒgbaren Demo-ZustĂ€nde: [`tools/README.md`](tools/README.md). + +Tests und statische Analyse: + +```bash +composer install +vendor/bin/phpunit +vendor/bin/phpstan analyse +``` + ## đŸ—ș Roadmap - [x] Voucher-Templates (vordefinierte Laufzeiten) @@ -447,11 +531,14 @@ setzen (`DB_*`, `APP_KEY`). - [x] Erweiterte Reporting-Funktionen (CSV/PDF) + Health-Endpoint - [x] 2FA-Recovery-Codes, API-Scopes/Rate-Limit/OpenAPI, Test-Suite (PHPUnit/PHPStan) - [x] Gemeinsames Design-System fĂŒr Frontend, Login und Backend +- [x] Branding ĂŒber die OberflĂ€che (Farben, Logo, Login-Seite) +- [x] Assets lokal ausliefern (keine Drittanbieter-CDNs) +- [x] VollstĂ€ndige englische Übersetzung des Admin-Bereichs ---
-**Version 2.5.0** · Autor: **Friederich Loheide** · Lizenz: **MIT** +**Version 2.6.0** · Autor: **Friederich Loheide** · Lizenz: **MIT**
diff --git a/admin/settings.php b/admin/settings.php index f81e013..1f191b4 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -243,6 +243,22 @@ $cs = [ 'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geĂ€ndert'), 'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"), 'print_template' => $db->getSetting('print_template', Ui::defaultPrintTemplate()), + 'brand_accent' => $db->getSetting('brand_accent', '') ?: Ui::DEFAULT_ACCENT, + 'brand_accent_dark' => $db->getSetting('brand_accent_dark', '') ?: Ui::DEFAULT_ACCENT_DARK, + 'brand_gradient_from' => $db->getSetting('brand_gradient_from', '') ?: Ui::DEFAULT_GRADIENT_FROM, + 'brand_gradient_to' => $db->getSetting('brand_gradient_to', '') ?: Ui::DEFAULT_GRADIENT_TO, + 'brand_radius' => $db->getSetting('brand_radius', (string)Ui::DEFAULT_RADIUS), + 'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'), + 'login_brand_name' => $db->getSetting('login_brand_name', ''), + 'login_logo_url' => $db->getSetting('login_logo_url', ''), + 'login_claim_title' => $db->getSetting('login_claim_title', ''), + 'login_claim_text' => $db->getSetting('login_claim_text', ''), + 'login_features' => $db->getSetting('login_features', ''), + 'login_footer' => $db->getSetting('login_footer', ''), + 'login_bg_image' => $db->getSetting('login_bg_image', ''), + 'login_bg_from' => $db->getSetting('login_bg_from', '#3b2f8f'), + 'login_bg_to' => $db->getSetting('login_bg_to', '#6d5ce7'), + 'login_bg_overlay' => $db->getSetting('login_bg_overlay', '40'), 'cron_token' => $db->getSetting('cron_token', ''), 'last_cron_sync' => $db->getSetting('last_cron_sync', ''), ]; @@ -250,8 +266,9 @@ $cs = [ /** * Bildfeld: Vorschau, Upload, alternativ URL – plus Entfernen-Schalter. */ -function imageField(string $name, string $label, string $value, string $hint = '', string $accept = 'image/*'): void +function imageField(string $name, string $label, ?string $value, string $hint = '', string $accept = 'image/*'): void { + $value = (string)$value; $preview = Ui::mediaUrl($value, '../'); ?>
@@ -278,6 +295,9 @@ function imageField(string $name, string $label, string $value, string $hint = ' @@ -310,20 +330,20 @@ $adminBase = '';
- - - - - + + + + + - - - - + + + +
-
+

@@ -342,7 +362,7 @@ $adminBase = '';
-
+

@@ -378,7 +398,7 @@ $adminBase = '';
-
+

@@ -447,7 +467,7 @@ $adminBase = '';
-
+

@@ -525,7 +545,7 @@ $adminBase = '';
-
+

@@ -589,7 +609,7 @@ $adminBase = '';
-
+

SMTP

@@ -620,7 +640,7 @@ $adminBase = '';
-
+

E-Mail Templates

@@ -641,7 +661,7 @@ $adminBase = '';
-
+

System & Erweitert

@@ -667,7 +687,7 @@ $adminBase = '';
-
+

@@ -731,7 +751,7 @@ document.querySelectorAll('.color-swatch').forEach(swatch => { }); }); -// Restore tab from hash +// Tab aus Anker uebernehmen (der Query-Parameter wird serverseitig gesetzt) window.addEventListener('DOMContentLoaded', function() { const hash = location.hash.substring(1); if (hash) { diff --git a/docker-compose.yml b/docker-compose.yml index 876fdc3..d341437 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,9 @@ services: # nicht mehr gelesen werden. Erzeugen: php -r "echo base64_encode(random_bytes(32));" APP_KEY: "" TZ: Europe/Berlin + volumes: + # Hochgeladene Logos/Hintergruende ueberleben so ein Image-Update + - uploads:/var/www/html/uploads depends_on: db: condition: service_healthy @@ -36,3 +39,4 @@ services: volumes: db_data: + uploads: diff --git a/docs/screenshots/admin-dashboard-dark.png b/docs/screenshots/admin-dashboard-dark.png index 4ee43ad..60d8402 100644 Binary files a/docs/screenshots/admin-dashboard-dark.png and b/docs/screenshots/admin-dashboard-dark.png differ diff --git a/docs/screenshots/admin-dashboard.png b/docs/screenshots/admin-dashboard.png index d08bc2e..60d8402 100644 Binary files a/docs/screenshots/admin-dashboard.png and b/docs/screenshots/admin-dashboard.png differ diff --git a/docs/screenshots/api-keys.png b/docs/screenshots/api-keys.png index 73b15da..73d2f00 100644 Binary files a/docs/screenshots/api-keys.png and b/docs/screenshots/api-keys.png differ diff --git a/docs/screenshots/bulk-vouchers.png b/docs/screenshots/bulk-vouchers.png index 8aa8009..e8252cd 100644 Binary files a/docs/screenshots/bulk-vouchers.png and b/docs/screenshots/bulk-vouchers.png differ diff --git a/docs/screenshots/integrations.png b/docs/screenshots/integrations.png index fbf5d31..7d68b10 100644 Binary files a/docs/screenshots/integrations.png and b/docs/screenshots/integrations.png differ diff --git a/docs/screenshots/login-branding.png b/docs/screenshots/login-branding.png index 7a941b8..56eb3f0 100644 Binary files a/docs/screenshots/login-branding.png and b/docs/screenshots/login-branding.png differ diff --git a/docs/screenshots/login.png b/docs/screenshots/login.png index 100d8f4..fcab8a3 100644 Binary files a/docs/screenshots/login.png and b/docs/screenshots/login.png differ diff --git a/docs/screenshots/maintenance.png b/docs/screenshots/maintenance.png index 4281ceb..52a70b7 100644 Binary files a/docs/screenshots/maintenance.png and b/docs/screenshots/maintenance.png differ diff --git a/docs/screenshots/mobile-vouchers.png b/docs/screenshots/mobile-vouchers.png new file mode 100644 index 0000000..bc29c3c Binary files /dev/null and b/docs/screenshots/mobile-vouchers.png differ diff --git a/docs/screenshots/settings-branding.png b/docs/screenshots/settings-branding.png new file mode 100644 index 0000000..5e5d511 Binary files /dev/null and b/docs/screenshots/settings-branding.png differ diff --git a/docs/screenshots/settings-login.png b/docs/screenshots/settings-login.png index f4256cb..5a110cb 100644 Binary files a/docs/screenshots/settings-login.png and b/docs/screenshots/settings-login.png differ diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png index fc43c4c..688a9ee 100644 Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ diff --git a/docs/screenshots/two-factor.png b/docs/screenshots/two-factor.png index ec2cbc4..eb57684 100644 Binary files a/docs/screenshots/two-factor.png and b/docs/screenshots/two-factor.png differ diff --git a/docs/screenshots/updater-available.png b/docs/screenshots/updater-available.png index 331ac34..0e3fec4 100644 Binary files a/docs/screenshots/updater-available.png and b/docs/screenshots/updater-available.png differ diff --git a/docs/screenshots/updater.png b/docs/screenshots/updater.png index 665c5d9..0e3fec4 100644 Binary files a/docs/screenshots/updater.png and b/docs/screenshots/updater.png differ diff --git a/docs/screenshots/voucher-form.png b/docs/screenshots/voucher-form.png index 60aedbe..776bbf5 100644 Binary files a/docs/screenshots/voucher-form.png and b/docs/screenshots/voucher-form.png differ diff --git a/docs/screenshots/voucher-result.png b/docs/screenshots/voucher-result.png index c4e7c0f..7119d7d 100644 Binary files a/docs/screenshots/voucher-result.png and b/docs/screenshots/voucher-result.png differ diff --git a/docs/screenshots/vouchers.png b/docs/screenshots/vouchers.png index bce6fe4..ce818ed 100644 Binary files a/docs/screenshots/vouchers.png and b/docs/screenshots/vouchers.png differ diff --git a/includes/.htaccess b/includes/.htaccess new file mode 100644 index 0000000..2ac81c4 --- /dev/null +++ b/includes/.htaccess @@ -0,0 +1,2 @@ +# Diese Dateien werden nur serverseitig eingebunden und nie direkt ausgeliefert. +Require all denied diff --git a/tests/.htaccess b/tests/.htaccess new file mode 100644 index 0000000..2ac81c4 --- /dev/null +++ b/tests/.htaccess @@ -0,0 +1,2 @@ +# Diese Dateien werden nur serverseitig eingebunden und nie direkt ausgeliefert. +Require all denied diff --git a/tools/.htaccess b/tools/.htaccess new file mode 100644 index 0000000..2ac81c4 --- /dev/null +++ b/tools/.htaccess @@ -0,0 +1,2 @@ +# Diese Dateien werden nur serverseitig eingebunden und nie direkt ausgeliefert. +Require all denied diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..b3ece02 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,42 @@ +# Werkzeuge + +## Demo-Instanz (`tools/demo/build.py`) + +Baut aus dem Projekt eine lauffĂ€hige Kopie **ohne Datenbank**: `Database` und +`Auth` werden durch Stubs mit festen Beispieldaten ersetzt (`tools/demo/overlay/`). +Das Original bleibt unverĂ€ndert. + +```bash +python3 tools/demo/build.py /tmp/uvt-demo +php -S 127.0.0.1:8123 -t /tmp/uvt-demo +``` + +NĂŒtzlich fĂŒr: + +* **Screenshots** fĂŒr die Dokumentation – immer mit denselben Daten +* **Smoke-Test** – jede Seite einmal rendern, ohne MySQL aufzusetzen + +Demo-spezifische ZustĂ€nde werden ĂŒber Query-Parameter erreicht: + +| Parameter | Wirkung | +|---|---| +| `?demo=result` / `?demo=bulk` | Voucher-Ergebnis bzw. Bulk-Liste auf `index.php` | +| `?demo=new` | frisch erzeugter SchlĂŒssel auf `admin/api_keys.php` | +| `?anon=1` | nicht angemeldet (fĂŒr die Login-Seite) | +| `?theme=dark` | erzwingt den Dark Mode | +| `?brand=custom` / `?brand=nopanel` | Beispiel-Branding der Login-Seite | + +## Screenshots (`tools/screenshots.py`) + +Rendert die Bilder aus `docs/screenshots/` neu. Voraussetzung ist ein +headless Chromium; der Pfad kommt aus `$CHROME_BIN` oder wird in den +ĂŒblichen Verzeichnissen gesucht. + +```bash +python3 tools/demo/build.py /tmp/uvt-demo +php -S 127.0.0.1:8123 -t /tmp/uvt-demo & +CHROME_BIN=/usr/bin/chromium python3 tools/screenshots.py +``` + +Die Skripte sind Hilfsmittel fĂŒr die Entwicklung – im Betrieb werden sie nicht +benötigt und sind per `.htaccess` nicht ĂŒber HTTP erreichbar. diff --git a/tools/screenshots.py b/tools/screenshots.py new file mode 100755 index 0000000..95938df --- /dev/null +++ b/tools/screenshots.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Erzeugt die Screenshots in docs/screenshots aus der laufenden Demo-Instanz. + +Voraussetzung: + python3 tools/demo/build.py /tmp/uvt-demo + php -S 127.0.0.1:8123 -t /tmp/uvt-demo & + +Aufruf: + python3 tools/screenshots.py [basis-url] +""" +import os +import shutil +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT = os.path.join(ROOT, 'docs', 'screenshots') +SCALE = 2 + +# (Datei, Pfad, Breite, Hoehe) – Breite/Hoehe in CSS-Pixeln, Ausgabe in 2x +SHOTS = [ + ('login.png', 'login.php?anon=1', 1200, 850), + ('login-branding.png', 'login.php?anon=1&brand=custom', 1200, 850), + ('voucher-form.png', 'index.php', 1200, 880), + ('voucher-result.png', 'index.php?demo=result', 1200, 900), + ('bulk-vouchers.png', 'index.php?demo=bulk', 1200, 800), + ('admin-dashboard.png', 'admin/index.php', 1200, 1400), + ('admin-dashboard-dark.png', 'admin/index.php?theme=dark', 1200, 1400), + ('vouchers.png', 'admin/vouchers.php', 1200, 1120), + ('settings.png', 'admin/settings.php', 1200, 950), + ('settings-login.png', 'admin/settings.php?tab=login', 1200, 1000), + ('settings-branding.png', 'admin/settings.php?tab=branding', 1200, 900), + ('api-keys.png', 'admin/api_keys.php?demo=new', 1200, 830), + ('integrations.png', 'admin/integrations.php', 1200, 900), + ('two-factor.png', 'admin/security.php', 1200, 900), + ('updater.png', 'admin/update.php', 1200, 780), + ('updater-available.png', 'admin/update.php?demo=available', 1200, 780), + ('mobile-vouchers.png', 'admin/users.php', 430, 860), + ('maintenance.png', 'updater/templates/maintenance.html', 1200, 700), +] + +CANDIDATES = [ + os.environ.get('CHROME_BIN', ''), + '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome', + '/opt/pw-browsers/chromium', shutil.which('chromium') or '', +] + + +def find_browser() -> str: + for path in CANDIDATES: + if path and os.path.isfile(path) and os.access(path, os.X_OK): + return path + raise SystemExit('Kein Chromium gefunden – Pfad ueber CHROME_BIN setzen.') + + +def main() -> int: + base = (sys.argv[1] if len(sys.argv) > 1 else 'http://127.0.0.1:8123').rstrip('/') + browser = find_browser() + os.makedirs(OUT, exist_ok=True) + + for name, path, width, height in SHOTS: + target = os.path.join(OUT, name) + subprocess.run([ + browser, '--headless', '--no-sandbox', '--disable-gpu', '--hide-scrollbars', + '--force-color-profile=srgb', '--disable-lcd-text', '--font-render-hinting=none', + '--virtual-time-budget=4000', '--force-prefers-reduced-motion', + '--force-device-scale-factor=%d' % SCALE, + '--window-size=%d,%d' % (width, height), + '--screenshot=%s' % target, + '%s/%s' % (base, path), + ], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print('%-28s %s' % (name, 'ok' if os.path.exists(target) else 'FEHLER')) + + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/updater/migrations/.htaccess b/updater/migrations/.htaccess new file mode 100644 index 0000000..2ac81c4 --- /dev/null +++ b/updater/migrations/.htaccess @@ -0,0 +1,2 @@ +# Diese Dateien werden nur serverseitig eingebunden und nie direkt ausgeliefert. +Require all denied diff --git a/updater/templates/maintenance.html b/updater/templates/maintenance.html index f983e57..6f08e4a 100644 --- a/updater/templates/maintenance.html +++ b/updater/templates/maintenance.html @@ -66,6 +66,9 @@ 100% { transform: translateX(300%); } } .hint { font-size: 12.5px; color: #8a93a3; } + @media (prefers-reduced-motion: reduce) { + .bar::after { animation: none; width: 45%; } + } @media (prefers-color-scheme: dark) { body { color: #e9ecf3; background: radial-gradient(760px 420px at 12% -8%, #1a1c30, transparent 62%), #0a0c11; } .card { background: #12151d; border-color: #232838; }