feat(setup): Systemcheck und Upload-Paket fuer klassisches Webhosting

Auf Shared Hosting gibt es keine Kommandozeile - ohne Anzeige laesst
sich dort nicht feststellen, ob etwa gd fehlt, die PHP-Version zu alt
ist oder data/ nicht beschreibbar ist. Der Setup-Assistent prueft das
jetzt und listet es auf; im Dashboard meldet sich die Pruefung spaeter
nur noch, wenn etwas nicht stimmt. Fehlende Pflichtbausteine sind
Fehler, fehlende Kuer (intl, imagick, cURL) nur Hinweise mit Angabe,
was dadurch entfaellt.

build-release.sh erzeugt das Upload-Paket: Abhaengigkeiten ohne
Entwicklungspakete, ohne Tests und Docker-Dateien. Dabei faellt eine
16 MB grosse Schriftart aus endroid/qr-code weg, die ausschliesslich
fuer Beschriftungen unter dem QR-Code gebraucht wird - die erzeugt
diese Anwendung nicht. Das Paket schrumpft damit von 18 MB auf 1 MB.
Sollte spaeter doch eine Beschriftung dazukommen, erkennt das Skript
das und behaelt die Datei.

Die Variante ohne eigenen Webroot (alles liegt im Domainverzeichnis,
die .htaccess im Stammverzeichnis leitet nach public/ um) war bisher
gebaut, aber nie erprobt. Sie ist jetzt gegen eine Hoster-Simulation
getestet: Ersteinrichtung, Kurzlinks, Weiterleitung, QR-Codes,
Statistik und die Sperren fuer src/, vendor/, data/ und .env.
This commit is contained in:
2026-08-10 15:32:13 +02:00
parent fa92817a88
commit c65c02cda5
9 changed files with 462 additions and 8 deletions
+71 -5
View File
@@ -82,15 +82,41 @@ Benutzername und Passwort.
Es wird **keine Shell** benötigt. Der `vendor/`-Ordner ist im Release-Paket enthalten. Es wird **keine Shell** benötigt. Der `vendor/`-Ordner ist im Release-Paket enthalten.
### Paket vorbereiten ### Upload-Paket erzeugen
Wenn du das Projekt aus dem Repository nutzt und Composer lokal (oder per Docker) hast:
```bash ```bash
composer install --no-dev --optimize-autoloader sh build-release.sh
``` ```
Danach enthält der Ordner alles, was hochgeladen werden muss. Ohne lokales PHP über das Anwendungs-Image (der schlanke `composer:2` hat kein `gd`
und bricht an der Plattformprüfung ab):
```bash
docker compose build
docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest sh build-release.sh
```
Ergebnis ist `build/urlshort-qr-gen-release/` – rund **1 MB, 217 Dateien**. Das Skript
installiert die Abhängigkeiten ohne Entwicklungspakete und entfernt eine 16 MB große
Schriftart aus `endroid/qr-code`, die nur für Beschriftungen *unter* dem QR-Code
gebraucht wird (die diese Anwendung nicht erzeugt). Sollte sich das einmal ändern,
merkt das Skript es und behält die Datei.
### Was hochgeladen wird
| | |
|---|---|
| `public/` | Webroot: `index.php`, `.htaccess`, `assets/` |
| `src/` | Anwendungscode |
| `templates/` | Seiten |
| `migrations/` | Datenbankschema |
| `vendor/` | Abhängigkeiten (ohne Entwicklungspakete) |
| `data/` | leer, muss **beschreibbar** sein – hier entstehen Datenbank und Logos |
| `.htaccess` | nur für Variante A2 nötig (siehe unten) |
| `.env` | selbst anlegen, Vorlage ist `.env.example` |
**Nicht** hochgeladen werden `tests/`, `docker/`, `Dockerfile`, `docker-compose.yml`,
`phpunit.xml`, `build-release.sh` und `.git/` – das Release-Paket enthält sie erst gar nicht.
### Variante A1 – Webroot kann auf `public/` gezeigt werden (empfohlen) ### Variante A1 – Webroot kann auf `public/` gezeigt werden (empfohlen)
@@ -131,6 +157,46 @@ Zusätzlich liegt in `data/` eine eigene `.htaccess` als zweite Absicherung.
> liefern. Falls nicht, unterstützt der Hoster `AllowOverride` nicht ausreichend – > liefern. Falls nicht, unterstützt der Hoster `AllowOverride` nicht ausreichend –
> dann bitte Variante A1 verwenden. > dann bitte Variante A1 verwenden.
### Beispiel: Hetzner Webhosting (konsoleH)
Stellvertretend für Anbieter mit eigenem Panel – bei IONOS, Strato, All-Inkl oder Netcup
heißen die Menüpunkte anders, die Schritte sind dieselben.
1. **PHP-Version prüfen/setzen** – in konsoleH unter *Einstellungen → PHP-Version*
mindestens **PHP 8.2** wählen. Steht dort noch 7.x oder 8.0, läuft die Anwendung nicht.
2. **Verzeichnis wählen.** Der Webspace hat pro Domain ein Verzeichnis unterhalb von
`/public_html`. Lässt sich das Zielverzeichnis der Domain frei setzen, nimm
**Variante A1** und zeige damit auf `…/kurzlinks/public`. Geht das nicht, nimm
**Variante A2** und lade alles direkt in das Domainverzeichnis.
3. **Hochladen** per SFTP (Zugangsdaten stehen in konsoleH unter *Benutzer*). Den
**Inhalt** von `build/urlshort-qr-gen-release/` übertragen, nicht den Ordner selbst.
4. **Schreibrechte** für `data/` setzen – im FTP-Programm Rechte `755` (bei manchen
Tarifen `775`). Ohne das legt die Anwendung keine Datenbank an.
5. **`.env` anlegen:** `.env.example` in `.env` umbenennen und mindestens eintragen:
```dotenv
APP_BASE_URL=https://kurz.deine-domain.de
SESSION_SECURE=true
```
6. **HTTPS aktivieren** – Hetzner stellt kostenlose Let’s-Encrypt-Zertifikate aus
(*Einstellungen → SSL*). Vorher `APP_BASE_URL` auf `http://` lassen, sonst zeigen
Kurzlinks auf eine Adresse, die der Browser nicht öffnet.
7. **Domain aufrufen.** Der Setup-Assistent prüft zuerst die Systemvoraussetzungen und
zeigt an, falls etwas fehlt. Er verlangt außerdem das Einmal-Token aus
`data/setup-token.txt` – diese Datei per FTP öffnen, den 16-stelligen Wert eintragen.
Danach wird sie automatisch gelöscht.
**Datenbank:** SQLite genügt und ist voreingestellt – die bei Hetzner enthaltenen
MySQL-Datenbanken werden nicht gebraucht. Wer sie trotzdem nutzen will, trägt in der
`.env` `DB_DRIVER=mysql` samt Zugangsdaten aus konsoleH ein.
**Was bei Hetzner erfahrungsgemäß passt:** `mod_rewrite`, `gd`, `mbstring`, `fileinfo`
und `pdo_sqlite` sind vorhanden. `imagick` ist es meist nicht – dann erscheinen
SVG-Logos nur im SVG-Download, PNG-Logos funktionieren normal. Der Setup-Assistent
zeigt den tatsächlichen Stand deines Tarifs an; verlass dich darauf statt auf diese
Aufzählung.
### Wichtige Hinweise zu Shared Hosting ### Wichtige Hinweise zu Shared Hosting
- **`mod_rewrite` muss aktiv sein.** Ohne Rewrites funktionieren die Kurzlinks nicht. - **`mod_rewrite` muss aktiv sein.** Ohne Rewrites funktionieren die Kurzlinks nicht.
+75
View File
@@ -0,0 +1,75 @@
#!/bin/sh
# -------------------------------------------------------------
# Erzeugt das Upload-Paket fuer klassisches Webhosting.
#
# Ergebnis: build/urlshort-qr-gen-release/ und build/urlshort-qr-gen.zip
# Enthalten ist alles, was auf dem Server gebraucht wird - inklusive
# vendor/ ohne Entwicklungspakete. Nicht enthalten sind Tests, Docker,
# Git-Dateien und die eigene .env.
#
# Aufruf mit lokalem PHP und Composer (im Projektverzeichnis):
# sh build-release.sh
#
# Ohne lokales PHP ueber das Anwendungs-Image - dieses bringt PHP samt
# gd, intl und Composer mit (der schlanke composer:2 hat kein gd und
# bricht deshalb an der Plattformpruefung ab):
# docker compose build
# docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest sh build-release.sh
# -------------------------------------------------------------
set -eu
ROOT="$(pwd)"
BUILD="$ROOT/build"
TARGET="$BUILD/urlshort-qr-gen-release"
echo "==> Aufraeumen"
rm -rf "$BUILD"
mkdir -p "$TARGET"
echo "==> Dateien kopieren"
for item in public src templates migrations composer.json composer.lock .env.example README.md .htaccess; do
cp -R "$ROOT/$item" "$TARGET/"
done
mkdir -p "$TARGET/data/uploads"
cp "$ROOT/data/.htaccess" "$TARGET/data/.htaccess"
: > "$TARGET/data/.gitkeep"
echo "==> Abhaengigkeiten ohne Entwicklungspakete installieren"
cd "$TARGET"
composer install --no-dev --optimize-autoloader --no-interaction --no-progress --quiet
# composer.lock wird auf dem Server nicht gebraucht, composer.json auch
# nicht - beide sind ueber die .htaccess ohnehin gesperrt. Sie bleiben
# liegen, damit sich das Paket spaeter aktualisieren laesst.
# endroid bringt eine 16 MB grosse Schriftart mit. Gebraucht wird sie
# ausschliesslich fuer Beschriftungen UNTER dem QR-Code (Label) - diese
# Anwendung erzeugt keine, der QrRenderer uebergibt dort immer null.
# Ohne die Datei schrumpft der Upload von rund 18 MB auf gut 2 MB.
# Sollte spaeter doch eine Beschriftung dazukommen, faellt das hier auf
# und die Schriftart bleibt im Paket.
echo "==> Ungenutzte Schriftart pruefen"
if grep -rqE 'Endroid\\\\QrCode\\\\Label|new Label\(' "$TARGET/src" 2>/dev/null; then
echo " Label wird verwendet - Schriftart bleibt erhalten."
else
rm -f "$TARGET/vendor/endroid/qr-code/assets/noto_sans.otf"
echo " noto_sans.otf entfernt (wird nicht verwendet)."
fi
echo "==> Archiv erzeugen"
cd "$BUILD"
if command -v zip >/dev/null 2>&1; then
zip -rq urlshort-qr-gen.zip urlshort-qr-gen-release
echo " build/urlshort-qr-gen.zip"
else
tar -czf urlshort-qr-gen.tar.gz urlshort-qr-gen-release
echo " build/urlshort-qr-gen.tar.gz (kein zip vorhanden)"
echo " Unter Windows laesst sich stattdessen ein ZIP erzeugen mit:"
echo " Compress-Archive build/urlshort-qr-gen-release build/urlshort-qr-gen.zip"
fi
echo "==> Fertig"
echo " Verzeichnis: build/urlshort-qr-gen-release"
echo " Dateien: $(find "$TARGET" -type f | wc -l)"
echo " Groesse: $(du -sh "$TARGET" | cut -f1)"
Generated
+3 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "3486c41b4f3b5ad75dcd8b39db645306", "content-hash": "f1b5bd346bb675500290d6e5402ce9ab",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -1847,7 +1847,8 @@
"php": ">=8.2", "php": ">=8.2",
"ext-json": "*", "ext-json": "*",
"ext-pdo": "*", "ext-pdo": "*",
"ext-gd": "*" "ext-gd": "*",
"ext-mbstring": "*"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.9.0"
+11 -1
View File
@@ -28,6 +28,7 @@ use App\Security\RateLimiter;
use App\Security\Session; use App\Security\Session;
use App\Security\SetupToken; use App\Security\SetupToken;
use App\Service\DomainChecker; use App\Service\DomainChecker;
use App\Service\SystemCheck;
use Throwable; use Throwable;
/** /**
@@ -137,9 +138,18 @@ final class App
$this->auth, $this->auth,
new SetupToken($this->config->path($this->config->getString('DATA_DIR', 'data'))), new SetupToken($this->config->path($this->config->getString('DATA_DIR', 'data'))),
$this->rateLimiter, $this->rateLimiter,
new SystemCheck($this->config),
); );
$login = new AuthController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->rateLimiter); $login = new AuthController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->rateLimiter);
$dashboard = new DashboardController($this->view, $this->auth, $this->links, $this->visits, $this->config, $this->domains); $dashboard = new DashboardController(
$this->view,
$this->auth,
$this->links,
$this->visits,
$this->config,
$this->domains,
new SystemCheck($this->config),
);
$linkController = new LinkController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->domains); $linkController = new LinkController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->domains);
$stats = new StatsController($this->config, $this->view, $this->auth, $this->links, $this->visits); $stats = new StatsController($this->config, $this->view, $this->auth, $this->links, $this->visits);
$qrController = new QrController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->qr); $qrController = new QrController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->qr);
+5
View File
@@ -12,6 +12,7 @@ use App\Repository\LinkRepository;
use App\Repository\VisitRepository; use App\Repository\VisitRepository;
use App\Security\Auth; use App\Security\Auth;
use App\Security\Session; use App\Security\Session;
use App\Service\SystemCheck;
use App\View; use App\View;
final class DashboardController extends Controller final class DashboardController extends Controller
@@ -23,6 +24,7 @@ final class DashboardController extends Controller
private readonly VisitRepository $visits, private readonly VisitRepository $visits,
Config $config, Config $config,
private readonly DomainRepository $domains, private readonly DomainRepository $domains,
private readonly SystemCheck $systemCheck,
) { ) {
parent::__construct($config, $view); parent::__construct($config, $view);
} }
@@ -46,6 +48,9 @@ final class DashboardController extends Controller
'topLinks' => $this->links->all('', 'clicks_desc', 5), 'topLinks' => $this->links->all('', 'clicks_desc', 5),
'series' => $this->visits->clicksPerDayGlobal(30), 'series' => $this->visits->clicksPerDayGlobal(30),
'domainCount' => $this->domains->count(), 'domainCount' => $this->domains->count(),
// Nur melden, wenn etwas nicht stimmt - im Normalfall
// soll die Uebersicht nicht mit Technik zugestellt werden.
'systemProblems' => $this->systemCheck->problems(),
'pageTitle' => 'Übersicht', 'pageTitle' => 'Übersicht',
]); ]);
} }
+4
View File
@@ -12,6 +12,7 @@ use App\Security\Csrf;
use App\Security\RateLimiter; use App\Security\RateLimiter;
use App\Security\Session; use App\Security\Session;
use App\Security\SetupToken; use App\Security\SetupToken;
use App\Service\SystemCheck;
use App\View; use App\View;
/** /**
@@ -37,6 +38,7 @@ final class SetupController extends Controller
private readonly Auth $auth, private readonly Auth $auth,
private readonly SetupToken $setupToken, private readonly SetupToken $setupToken,
private readonly RateLimiter $rateLimiter, private readonly RateLimiter $rateLimiter,
private readonly SystemCheck $systemCheck,
) { ) {
parent::__construct($config, $view); parent::__construct($config, $view);
} }
@@ -126,6 +128,8 @@ final class SetupController extends Controller
'errors' => $errors, 'errors' => $errors,
'minLength' => self::MIN_PASSWORD_LENGTH, 'minLength' => self::MIN_PASSWORD_LENGTH,
'tokenPath' => $this->setupToken->path(), 'tokenPath' => $this->setupToken->path(),
'systemChecks' => $this->systemCheck->all(),
'systemStatus' => $this->systemCheck->worstStatus(),
], $status, 'layout_plain'); ], $status, 'layout_plain');
} }
} }
+221
View File
@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Config;
/**
* Prueft, ob der Server alles mitbringt.
*
* Auf klassischem Webhosting gibt es keine Kommandozeile - ohne diese
* Anzeige liesse sich nicht feststellen, ob etwa gd fehlt oder das
* Datenverzeichnis nicht beschreibbar ist. Die Pruefung laeuft deshalb
* im Setup-Assistenten und meldet sich spaeter im Dashboard nur dann,
* wenn etwas nicht stimmt.
*/
final class SystemCheck
{
public const OK = 'ok';
public const WARNING = 'warning';
public const ERROR = 'error';
public const MIN_PHP = '8.2.0';
public function __construct(private readonly Config $config)
{
}
/**
* @return list<array{label:string,status:string,text:string,required:bool}>
*/
public function all(): array
{
return [
$this->php(),
$this->database(),
$this->extension('gd', 'Bilderzeugung (gd)', true, 'Ohne gd lassen sich keine PNG-QR-Codes erzeugen; SVG funktioniert weiterhin.'),
$this->extension('mbstring', 'Zeichenketten (mbstring)', true, 'Wird für Umlaute in Slugs, URLs und Domains gebraucht.'),
$this->extension('fileinfo', 'Dateityperkennung (fileinfo)', true, 'Wird beim Prüfen hochgeladener Logos gebraucht.'),
$this->dataDirectory(),
$this->uploadLimit(),
$this->extension('intl', 'Umlaut-Domains (intl)', false, 'Ohne intl greift ein eingebauter Punycode-Rückfallweg – Umlaut-Domains funktionieren trotzdem.'),
$this->extension('imagick', 'SVG-Logos in PNG (imagick)', false, 'Ohne imagick erscheinen SVG-Logos nur im SVG-Download. PNG-Logos sind davon nicht betroffen.'),
$this->outgoingRequests(),
];
}
/**
* Fasst zusammen, ob etwas zu tun ist.
*/
public function worstStatus(): string
{
$status = self::OK;
foreach ($this->all() as $check) {
if ($check['status'] === self::ERROR) {
return self::ERROR;
}
if ($check['status'] === self::WARNING) {
$status = self::WARNING;
}
}
return $status;
}
/**
* @return list<array{label:string,status:string,text:string,required:bool}>
*/
public function problems(): array
{
return array_values(array_filter(
$this->all(),
static fn (array $check): bool => $check['status'] !== self::OK,
));
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function php(): array
{
$ok = version_compare(PHP_VERSION, self::MIN_PHP, '>=');
return [
'label' => 'PHP-Version',
'status' => $ok ? self::OK : self::ERROR,
'text' => $ok
? PHP_VERSION
: sprintf('%s – benötigt wird mindestens %s. Im Hosting-Panel umstellen.', PHP_VERSION, self::MIN_PHP),
'required' => true,
];
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function database(): array
{
$driver = strtolower($this->config->getString('DB_DRIVER', 'sqlite'));
$needed = $driver === 'mysql' || $driver === 'mariadb' ? 'pdo_mysql' : 'pdo_sqlite';
$ok = extension_loaded('pdo') && extension_loaded($needed);
return [
'label' => 'Datenbank',
'status' => $ok ? self::OK : self::ERROR,
'text' => $ok
? sprintf('%s über %s', $driver === 'sqlite' ? 'SQLite' : 'MySQL/MariaDB', $needed)
: sprintf('Die PHP-Erweiterung %s fehlt.', $needed),
'required' => true,
];
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function extension(string $name, string $label, bool $required, string $hint): array
{
$loaded = extension_loaded($name);
return [
'label' => $label,
'status' => $loaded ? self::OK : ($required ? self::ERROR : self::WARNING),
'text' => $loaded ? 'vorhanden' : ('nicht vorhanden – ' . $hint),
'required' => $required,
];
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function dataDirectory(): array
{
$path = $this->config->path($this->config->getString('DATA_DIR', 'data'));
$writable = is_dir($path) && is_writable($path);
return [
'label' => 'Datenverzeichnis',
'status' => $writable ? self::OK : self::ERROR,
'text' => $writable
? $path . ' ist beschreibbar'
: sprintf('%s ist nicht beschreibbar. Rechte auf 755 bzw. 775 setzen.', $path),
'required' => true,
];
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function uploadLimit(): array
{
$wanted = $this->config->getInt('UPLOAD_MAX_BYTES', 2097152);
$limit = min(
self::toBytes((string) ini_get('upload_max_filesize')),
self::toBytes((string) ini_get('post_max_size')),
);
$ok = $limit <= 0 || $limit >= $wanted;
return [
'label' => 'Upload-Größe',
'status' => $ok ? self::OK : self::WARNING,
'text' => $ok
? sprintf('bis %s je Logo', self::format($limit > 0 ? $limit : $wanted))
: sprintf(
'Der Server erlaubt nur %s, eingestellt sind %s. Logos werden entsprechend früher abgelehnt.',
self::format($limit),
self::format($wanted),
),
'required' => false,
];
}
/**
* @return array{label:string,status:string,text:string,required:bool}
*/
private function outgoingRequests(): array
{
$curl = function_exists('curl_init');
$fopen = (bool) ini_get('allow_url_fopen');
$ok = $curl || $fopen;
return [
'label' => 'Domain-Prüfung',
'status' => $ok ? self::OK : self::WARNING,
'text' => $ok
? ($curl ? 'über cURL möglich' : 'über allow_url_fopen möglich')
: 'Weder cURL noch allow_url_fopen verfügbar – die Domain-Prüfung kann die Zuordnung nicht testen. '
. 'Alles Übrige funktioniert davon unberührt.',
'required' => false,
];
}
private static function toBytes(string $value): int
{
$value = trim($value);
if ($value === '') {
return 0;
}
$unit = strtolower(substr($value, -1));
$number = (int) $value;
return match ($unit) {
'g' => $number * 1024 * 1024 * 1024,
'm' => $number * 1024 * 1024,
'k' => $number * 1024,
default => $number,
};
}
private static function format(int $bytes): string
{
if ($bytes >= 1024 * 1024) {
return round($bytes / 1024 / 1024, 1) . ' MB';
}
return max(1, intdiv($bytes, 1024)) . ' KB';
}
}
+32
View File
@@ -10,10 +10,42 @@
*/ */
use App\Repository\LinkRepository; use App\Repository\LinkRepository;
use App\Service\SystemCheck;
use App\Support\ShortDomain; use App\Support\ShortDomain;
$fallbackBase = rtrim($config->baseUrl(), '/'); $fallbackBase = rtrim($config->baseUrl(), '/');
?> ?>
<?php if (($systemProblems ?? []) !== []):
$hatFehler = false;
foreach ($systemProblems as $problem) {
if ($problem['status'] === SystemCheck::ERROR) {
$hatFehler = true;
}
}
?>
<section class="card check-result check-result--<?= $hatFehler ? 'error' : 'warn' ?>">
<div class="check-result__head">
<span class="check-result__icon" aria-hidden="true"><?= $hatFehler ? '×' : '!' ?></span>
<div>
<h2>Hinweise zum Server</h2>
<p class="check-result__summary">
<?= $hatFehler
? 'Es fehlt etwas, das die Anwendung braucht.'
: 'Alles läuft – einzelne Zusatzfunktionen stehen aber nicht zur Verfügung.' ?>
</p>
</div>
</div>
<ul class="check-list">
<?php foreach ($systemProblems as $problem): ?>
<li class="check-list__item check-list__item--<?= $problem['status'] === SystemCheck::ERROR ? 'error' : 'warn' ?>">
<span class="check-list__icon" aria-hidden="true"><?= $problem['status'] === SystemCheck::ERROR ? '×' : '!' ?></span>
<span class="check-list__label"><?= $e($problem['label']) ?></span>
<span class="check-list__text"><?= $e($problem['text']) ?></span>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<div class="page-head"> <div class="page-head">
<div> <div>
<h1>Übersicht</h1> <h1>Übersicht</h1>
+40
View File
@@ -6,8 +6,22 @@
* @var array<string,string> $errors * @var array<string,string> $errors
* @var int $minLength * @var int $minLength
* @var string $tokenPath * @var string $tokenPath
* @var list<array{label:string,status:string,text:string,required:bool}> $systemChecks
* @var string $systemStatus
*/ */
use App\Service\SystemCheck;
$symbol = static fn (string $status): string => match ($status) {
SystemCheck::OK => '✓',
SystemCheck::WARNING => '!',
default => '×',
};
$cssClass = static fn (string $status): string => match ($status) {
SystemCheck::OK => 'ok',
SystemCheck::WARNING => 'warn',
default => 'error',
};
?> ?>
<div class="card card--narrow"> <div class="card card--narrow">
<div class="card__head"> <div class="card__head">
@@ -83,3 +97,29 @@
(<code>ADMIN_USER</code> und <code>ADMIN_PASSWORD_HASH</code>). Details stehen in der README. (<code>ADMIN_USER</code> und <code>ADMIN_PASSWORD_HASH</code>). Details stehen in der README.
</p> </p>
</div> </div>
<section class="card check-result check-result--<?= $e($cssClass($systemStatus)) ?>">
<details <?= $systemStatus === SystemCheck::OK ? '' : 'open' ?>>
<summary>
Systemvoraussetzungen –
<?= $systemStatus === SystemCheck::OK
? 'alles vorhanden'
: ($systemStatus === SystemCheck::WARNING ? 'mit Einschränkungen nutzbar' : 'es fehlt etwas') ?>
</summary>
<ul class="check-list">
<?php foreach ($systemChecks as $check): ?>
<li class="check-list__item check-list__item--<?= $e($cssClass($check['status'])) ?>">
<span class="check-list__icon" aria-hidden="true"><?= $e($symbol($check['status'])) ?></span>
<span class="check-list__label"><?= $e($check['label']) ?></span>
<span class="check-list__text"><?= $e($check['text']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<p class="muted small">
Dass diese Seite überhaupt erscheint, belegt bereits, dass <code>mod_rewrite</code> arbeitet
und die <code>.htaccess</code> ausgewertet wird.
</p>
</details>
</section>