Files
deskhub/DeskHub-Webseite/install/index.php
T
2026-10-16 19:12:18 +00:00

911 lines
33 KiB
PHP
Raw 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.
<?php
/**
* install/index.php
*
* Eigenständiger Installations-Assistent (unabhängig von includes/bootstrap.php,
* da config/config.php zu diesem Zeitpunkt ja erst erstellt wird).
*
* Nach erfolgreicher Installation sollte dieses Verzeichnis aus
* Sicherheitsgründen vom Server gelöscht oder zumindest gesperrt werden.
*/
session_start();
define('ROOT_DIR', dirname(__DIR__));
define('LOCK_FILE', __DIR__ . '/.installed');
require_once ROOT_DIR . '/database/sqlite_schema.php';
function h(?string $s): string
{
return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
}
// URL der aktuellen Installation automatisch erkennen
$detectedUrl = (function(): string {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
// install/ vom Pfad entfernen → Root-URL
$path = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
$path = preg_replace('#/install$#', '', $path);
return rtrim($scheme . '://' . $host . $path, '/');
})();
/**
* Fehlt die Sperrdatei (z. B. weil Dateien neu hochgeladen wurden), darf der Assistent trotzdem keine bestehende
* Installation überschreiben: Gibt es schon eine Konfiguration mit Datenbank und Mitarbeiterkonten, gilt sie als installiert.
*/
$existingInstallation = (function (): bool {
$cfgFile = ROOT_DIR . '/config/config.php';
if (!is_file($cfgFile)) return false;
try {
$cfg = (array) (require $cfgFile);
$db = (array) ($cfg['db'] ?? []);
if (($db['driver'] ?? 'sqlite') === 'mysql') {
$pdo = new PDO('mysql:host=' . ($db['host'] ?? 'localhost') . ';dbname=' . ($db['name'] ?? '') . ';charset=utf8mb4', (string) ($db['user'] ?? ''), (string) ($db['pass'] ?? ''), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 3]);
} else {
$path = (string) ($db['sqlite_path'] ?? '');
if ($path === '' || !is_file($path)) return false;
$pdo = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
return (int) $pdo->query('SELECT COUNT(*) FROM staff')->fetchColumn() > 0;
} catch (Throwable $e) {
return false; // keine nutzbare Datenbank: Erstinstallation ist in Ordnung
}
})();
$alreadyInstalled = file_exists(LOCK_FILE) || $existingInstallation;
$errors = [];
$success = false;
// ---------------------------------------------------------------
// Anforderungsprüfung
// ---------------------------------------------------------------
$checks = [
['label' => 'PHP-Version 8.1 oder neuer (gefunden: ' . PHP_VERSION . ')', 'ok' => version_compare(PHP_VERSION, '8.1.0', '>=')],
['label' => 'PDO-Erweiterung', 'ok' => extension_loaded('pdo')],
['label' => 'PDO MySQL-Treiber (für Produktivbetrieb)', 'ok' => extension_loaded('pdo_mysql'), 'optional' => true],
['label' => 'PDO SQLite-Treiber (optional, für lokale Tests)', 'ok' => extension_loaded('pdo_sqlite'), 'optional' => true],
['label' => 'mbstring-Erweiterung', 'ok' => extension_loaded('mbstring')],
['label' => 'IMAP-Erweiterung (optional, nur für E-Mail-zu-Ticket)', 'ok' => extension_loaded('imap'), 'optional' => true],
['label' => 'Verzeichnis config/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/config')],
['label' => 'Verzeichnis uploads/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/uploads')],
['label' => 'Verzeichnis database/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/database')],
];
$requiredOk = true;
foreach ($checks as $c) {
if (empty($c['optional']) && !$c['ok']) {
$requiredOk = false;
}
}
// ---------------------------------------------------------------
// Installation durchführen
// ---------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$alreadyInstalled && $requiredOk) {
if (empty($_SESSION['install_csrf']) || !hash_equals($_SESSION['install_csrf'], $_POST['csrf_token'] ?? '')) {
$errors[] = 'Ungültige Anfrage (Sitzung abgelaufen). Bitte die Seite neu laden und erneut versuchen.';
} else {
$driver = ($_POST['driver'] ?? '') === 'mysql' ? 'mysql' : 'sqlite';
$siteName = trim($_POST['site_name'] ?? '') ?: 'DeskHub Support';
$siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/');
$adminName = trim($_POST['admin_name'] ?? '');
$adminEmail = trim($_POST['admin_email'] ?? '');
$adminPass = $_POST['admin_password'] ?? '';
$adminPass2 = $_POST['admin_password2'] ?? '';
if ($siteUrl === '') $errors[] = 'Bitte die URL der Installation angeben (z.B. https://support.firma.de).';
if ($adminName === '') $errors[] = 'Bitte einen Namen für das Admin-Konto angeben.';
if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte eine gültige Admin-E-Mail-Adresse angeben.';
if (strlen($adminPass) < 8) $errors[] = 'Das Admin-Passwort muss mindestens 8 Zeichen lang sein.';
if ($adminPass !== $adminPass2) $errors[] = 'Die beiden Passwörter stimmen nicht überein.';
if (!extension_loaded('pdo_' . $driver)) $errors[] = "Die PHP-Erweiterung pdo_{$driver} ist nicht installiert.";
$dbConfig = [
'driver' => $driver,
'host' => 'localhost',
'name' => 'deskhub',
'user' => 'deskhub_user',
'pass' => 'change_me',
'sqlite_path' => ROOT_DIR . '/database/deskhub.sqlite',
];
if ($driver === 'mysql') {
$dbConfig['host'] = trim($_POST['db_host'] ?? 'localhost') ?: 'localhost';
$dbConfig['name'] = trim($_POST['db_name'] ?? '');
$dbConfig['user'] = trim($_POST['db_user'] ?? '');
$dbConfig['pass'] = $_POST['db_pass'] ?? '';
if ($dbConfig['name'] === '' || $dbConfig['user'] === '') {
$errors[] = 'Bitte Datenbankname und Datenbankbenutzer angeben.';
}
}
$pdo = null;
if (!$errors) {
try {
if ($driver === 'mysql') {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $dbConfig['host'], $dbConfig['name']);
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass']);
} else {
@unlink($dbConfig['sqlite_path']);
$pdo = new PDO('sqlite:' . $dbConfig['sqlite_path']);
$pdo->exec('PRAGMA foreign_keys = ON;');
}
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (Throwable $e) {
$errors[] = 'Datenbankverbindung fehlgeschlagen: ' . $e->getMessage();
}
}
if (!$errors && $pdo) {
try {
$sql = file_get_contents(ROOT_DIR . '/database/schema.sql');
if ($driver === 'sqlite') {
$sql = convert_schema_to_sqlite($sql);
}
$pdo->exec($sql);
// Vordefiniertes Demo-Admin-Konto entfernen und durch das hier
// eingegebene, echte Admin-Konto ersetzen.
$pdo->exec("DELETE FROM staff WHERE email = 'admin@example.com'");
$stmt = $pdo->prepare(
'INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)'
);
$stmt->execute([$adminName, $adminEmail, password_hash($adminPass, PASSWORD_DEFAULT), 'admin', 1, date('Y-m-d H:i:s')]);
// ── Initiale Einstellungen in die DB seeden ───────────────────────
// Alle Kerneinstellungen sofort in der settings-Tabelle anlegen,
// damit sie Updates überleben (config.php wird beim Update nie
// überschrieben, aber so sind die Werte auch bei Neuinstallationen
// sofort über get_setting() abrufbar).
// Sicherer, zufälliger App-Key – direkt hier generieren, damit er
// sowohl in die DB (überlebt Updates) als auch in config.php geschrieben wird.
$appKey = bin2hex(random_bytes(32));
$installSettings = [
'app_key' => $appKey, // ← in DB: überlebt Updates
'display_name' => $siteName,
'site_url' => $siteUrl,
'timezone' => 'Europe/Berlin',
'smtp_from_email' => $adminEmail,
'smtp_from_name' => $siteName,
'mail_enabled' => '0',
'helpdesk_status' => 'online',
'kb_enabled' => '1',
'ticket_ratings_enabled' => '1',
'staff_session_timeout' => '30',
'retention_enabled' => '0',
'retention_months' => '24',
'ip_anonymize' => '0',
'upload_allowed_extensions' => 'png, jpg, jpeg, gif, pdf, txt, log, docx, xlsx, zip',
'upload_max_size_mb' => '10',
];
$upsertSql = $driver === 'sqlite'
? 'INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?, ?)'
: 'INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)';
foreach ($installSettings as $k => $v) {
$pdo->prepare($upsertSql)->execute([$k, $v]);
}
// Alle vorhandenen Migrationsdateien sofort als "eingespielt" markieren,
// da das Schema gerade frisch erstellt wurde.
$migrTable = $driver === 'sqlite'
? "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT (datetime('now')))"
: "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$pdo->exec($migrTable);
$insertMigr = $driver === 'sqlite'
? 'INSERT OR IGNORE INTO schema_migrations (migration) VALUES (?)'
: 'INSERT IGNORE INTO schema_migrations (migration) VALUES (?)';
foreach (glob(ROOT_DIR . '/database/migrations/*.sql') ?: [] as $mf) {
$pdo->prepare($insertMigr)->execute([basename($mf)]);
}
} catch (Throwable $e) {
$errors[] = 'Fehler beim Einrichten der Datenbank: ' . $e->getMessage();
}
}
if (!$errors) {
$configArray = [
'app_key' => $appKey,
'db' => $dbConfig,
'site' => [
'name' => $siteName,
'url' => $siteUrl,
'admin_email' => $adminEmail,
'timezone' => 'Europe/Berlin',
],
'mail' => [
'enabled' => false,
'from_email' => $adminEmail,
'from_name' => $siteName,
],
'imap' => [
'enabled' => false,
'mailbox' => '{imap.example.com:993/imap/ssl}INBOX',
'user' => 'support@example.com',
'pass' => 'change_me',
'default_department_id' => 1,
'default_priority_id' => 2,
],
'uploads' => [
'max_size_bytes' => 10 * 1024 * 1024,
'allowed_extensions' => ['png','jpg','jpeg','gif','pdf','txt','log','docx','xlsx','zip'],
'path' => ROOT_DIR . '/uploads',
],
];
$php = "<?php\n/**\n * DeskHub – Konfiguration\n"
. " * Automatisch erstellt durch den Installations-Assistenten am " . date('d.m.Y H:i') . "\n"
. " * WICHTIG: app_key niemals ändern – verschlüsselte Werte in der DB werden sonst unlesbar.\n"
. " */\n\nreturn " . var_export($configArray, true) . ";\n";
if (file_put_contents(ROOT_DIR . '/config/config.php', $php) === false) {
$errors[] = 'config/config.php konnte nicht geschrieben werden. Bitte Schreibrechte des Verzeichnisses prüfen.';
} else {
file_put_contents(LOCK_FILE, date('Y-m-d H:i:s') . " von " . ($_SERVER['REMOTE_ADDR'] ?? 'CLI'));
$success = true;
}
}
}
}
if (empty($_SESSION['install_csrf'])) {
$_SESSION['install_csrf'] = bin2hex(random_bytes(32));
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DeskHub – Installation</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--ink: #16202a;
--primary: #0f6f6a;
--primary-dk:#0b5853;
--primary-lt:#e6f4f3;
--accent: #d97706;
--danger: #dc2626;
--danger-lt: #fef2f2;
--success: #16a34a;
--success-lt:#f0fdf4;
--border: #dde3ec;
--muted: #64748b;
--surface: #ffffff;
--bg: #f1f5f9;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--radius: 10px;
--shadow: 0 4px 24px rgba(15,23,42,.10), 0 1px 4px rgba(15,23,42,.06);
}
html, body {
min-height: 100vh;
font-family: var(--font);
font-size: 15px;
line-height: 1.55;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
/* ── Layout ── */
.i-shell {
display: flex;
min-height: 100vh;
}
/* ── Left brand panel ── */
.i-brand {
width: 340px;
flex-shrink: 0;
background: var(--ink);
background-image: radial-gradient(ellipse at 20% 50%, rgba(15,111,106,.35) 0%, transparent 65%),
radial-gradient(ellipse at 80% 10%, rgba(217,119,6,.15) 0%, transparent 55%);
display: flex;
flex-direction: column;
padding: 48px 36px;
position: sticky;
top: 0;
height: 100vh;
}
.i-brand-logo {
font-size: 1.5rem;
font-weight: 800;
color: #fff;
letter-spacing: -.03em;
margin-bottom: 8px;
}
.i-brand-logo span { color: var(--accent); }
.i-brand-tagline {
font-size: .82rem;
color: #94a3b8;
margin-bottom: 48px;
}
.i-brand-features {
list-style: none;
display: flex;
flex-direction: column;
gap: 18px;
flex: 1;
}
.i-brand-features li {
display: flex;
gap: 12px;
align-items: flex-start;
}
.i-brand-feat-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
flex-shrink: 0;
margin-top: 1px;
}
.i-brand-feat-text strong {
display: block;
font-size: .82rem;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 2px;
}
.i-brand-feat-text span {
font-size: .75rem;
color: #64748b;
}
.i-brand-footer {
font-size: .72rem;
color: #334155;
margin-top: 32px;
}
/* ── Right content area ── */
.i-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 24px 64px;
overflow-y: auto;
}
.i-box {
width: 100%;
max-width: 560px;
}
/* ── Section title ── */
.i-section-title {
font-size: .7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--primary);
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary-lt);
display: flex;
align-items: center;
gap: 8px;
}
/* ── Cards ── */
.i-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 28px 28px 24px;
margin-bottom: 20px;
}
.i-card-title {
font-size: 1rem;
font-weight: 700;
color: var(--ink);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.i-card-title .num {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--primary);
color: #fff;
font-size: .72rem;
font-weight: 800;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ── Check list ── */
.i-checks { list-style: none; display: flex; flex-direction: column; gap: 0; }
.i-check-row {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 0;
border-bottom: 1px solid var(--border);
font-size: .875rem;
}
.i-check-row:last-child { border-bottom: none; }
.i-check-badge {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: .7rem;
font-weight: 800;
flex-shrink: 0;
}
.i-check-badge.ok { background: var(--success-lt); color: var(--success); }
.i-check-badge.err { background: var(--danger-lt); color: var(--danger); }
.i-check-badge.opt { background: #f1f5f9; color: var(--muted); }
.i-check-row.optional { opacity: .65; }
/* ── Form elements ── */
.i-field { margin-bottom: 16px; }
.i-field label {
display: block;
font-size: .8rem;
font-weight: 600;
color: #374151;
margin-bottom: 5px;
}
.i-field input,
.i-field select {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 7px;
font-size: .9rem;
font-family: var(--font);
color: var(--ink);
background: #fff;
transition: border-color .15s, box-shadow .15s;
outline: none;
}
.i-field input:focus,
.i-field select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(15,111,106,.12);
}
.i-field input::placeholder { color: #b0bac6; }
.i-field .i-hint {
font-size: .74rem;
color: var(--muted);
margin-top: 4px;
}
.i-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* ── DB driver tabs ── */
.i-driver-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.i-driver-tab {
flex: 1;
border: 2px solid var(--border);
border-radius: 9px;
padding: 12px 14px;
cursor: pointer;
background: #fff;
transition: border-color .15s, background .15s;
text-align: left;
}
.i-driver-tab input[type=radio] { display: none; }
.i-driver-tab.active {
border-color: var(--primary);
background: var(--primary-lt);
}
.i-driver-tab-label {
font-size: .85rem;
font-weight: 700;
color: var(--ink);
margin-bottom: 2px;
}
.i-driver-tab-desc {
font-size: .73rem;
color: var(--muted);
}
.i-driver-tab.active .i-driver-tab-label { color: var(--primary-dk); }
/* ── Submit button ── */
.i-submit-btn {
width: 100%;
padding: 13px;
background: var(--primary);
color: #fff;
font-size: .95rem;
font-weight: 700;
font-family: var(--font);
border: none;
border-radius: 9px;
cursor: pointer;
transition: background .15s, transform .1s;
margin-top: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
letter-spacing: .01em;
}
.i-submit-btn:hover { background: var(--primary-dk); }
.i-submit-btn:active { transform: scale(.99); }
/* ── Alerts ── */
.i-alert {
padding: 12px 16px;
border-radius: 8px;
font-size: .875rem;
margin-bottom: 14px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.i-alert.error { background: var(--danger-lt); border: 1px solid #fca5a5; color: #991b1b; }
.i-alert.success { background: var(--success-lt); border: 1px solid #86efac; color: #166534; }
.i-alert.warning { background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; }
.i-alert-icon { font-size: 1rem; line-height: 1.4; flex-shrink: 0; }
/* ── Success screen ── */
.i-success-icon {
width: 64px;
height: 64px;
background: var(--success-lt);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
margin: 0 auto 20px;
}
.i-action-btns { display: flex; gap: 12px; margin-top: 20px; }
.i-btn-primary {
flex: 1;
padding: 11px;
background: var(--primary);
color: #fff;
border-radius: 8px;
font-weight: 700;
font-size: .875rem;
text-align: center;
text-decoration: none;
transition: background .15s;
}
.i-btn-primary:hover { background: var(--primary-dk); color: #fff; text-decoration: none; }
.i-btn-secondary {
flex: 1;
padding: 11px;
background: #fff;
color: var(--ink);
border: 1px solid var(--border);
border-radius: 8px;
font-weight: 600;
font-size: .875rem;
text-align: center;
text-decoration: none;
transition: background .15s;
}
.i-btn-secondary:hover { background: var(--bg); color: var(--ink); text-decoration: none; }
code {
font-family: var(--mono);
font-size: .82em;
background: #f1f5f9;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--border);
}
.i-divider {
border: none;
border-top: 1px solid var(--border);
margin: 20px 0;
}
/* ── Password strength ── */
.i-pw-bar {
height: 4px;
border-radius: 2px;
background: var(--border);
margin-top: 6px;
overflow: hidden;
}
.i-pw-fill {
height: 100%;
border-radius: 2px;
width: 0%;
transition: width .3s, background .3s;
}
@media (max-width: 768px) {
.i-shell { flex-direction: column; }
.i-brand { width: 100%; height: auto; position: static; padding: 28px 24px; }
.i-brand-features { display: none; }
.i-content { padding: 28px 16px 48px; }
.i-row { grid-template-columns: 1fr; gap: 0; }
}
</style>
</head>
<body>
<div class="i-shell">
<!-- ── Brand Sidebar ── -->
<aside class="i-brand">
<div class="i-brand-logo">Desk<span>Hub</span></div>
<div class="i-brand-tagline">Helpdesk & Ticketsystem</div>
<ul class="i-brand-features">
<li>
<div class="i-brand-feat-icon">🎫</div>
<div class="i-brand-feat-text">
<strong>Ticket-Management</strong>
<span>Abteilungen, Prioritäten, SLA, Regeln</span>
</div>
</li>
<li>
<div class="i-brand-feat-icon">📧</div>
<div class="i-brand-feat-text">
<strong>E-Mail-Integration</strong>
<span>SMTP-Versand & IMAP-Abruf</span>
</div>
</li>
<li>
<div class="i-brand-feat-icon">👥</div>
<div class="i-brand-feat-text">
<strong>Kundenportal</strong>
<span>Self-Service, Wissensdatenbank</span>
</div>
</li>
<li>
<div class="i-brand-feat-icon">📊</div>
<div class="i-brand-feat-text">
<strong>Berichte & Statistiken</strong>
<span>SLA-Raten, Agenten-Performance</span>
</div>
</li>
<li>
<div class="i-brand-feat-icon">🔔</div>
<div class="i-brand-feat-text">
<strong>Benachrichtigungen</strong>
<span>Push, Discord, Telegram</span>
</div>
</li>
<li>
<div class="i-brand-feat-icon">🔒</div>
<div class="i-brand-feat-text">
<strong>Sicherheit</strong>
<span>2FA, verschlüsselte Zugangsdaten</span>
</div>
</li>
</ul>
<div class="i-brand-footer">DeskHub · Viper Network</div>
</aside>
<!-- ── Main content ── -->
<main class="i-content">
<div class="i-box">
<?php if ($alreadyInstalled): ?>
<!-- Already installed -->
<div class="i-card" style="text-align:center;padding:40px 28px">
<div class="i-success-icon">🔒</div>
<h2 style="margin-bottom:10px">Bereits installiert</h2>
<p style="color:var(--muted);font-size:.9rem;margin-bottom:6px">
Eingerichtet am <strong><?= h(is_file(LOCK_FILE) ? (string) file_get_contents(LOCK_FILE) : "bereits vorhandener Konfiguration") ?></strong>
</p>
<div class="i-alert warning" style="margin-top:20px;text-align:left">
<span class="i-alert-icon">⚠️</span>
<span>Bitte das Verzeichnis <code>install/</code> vom Server löschen, um diese Seite zu deaktivieren.</span>
</div>
<div class="i-action-btns">
<a href="/login.php" class="i-btn-primary">Kundenportal</a>
<a href="/staff/login.php" class="i-btn-secondary">Mitarbeiter-Login</a>
</div>
<p style="font-size:.75rem;color:var(--muted);margin-top:16px">
Zum Wiederholen: <code>install/.installed</code> löschen
</p>
</div>
<?php elseif ($success): ?>
<!-- Success screen -->
<div class="i-card" style="text-align:center;padding:40px 28px">
<div class="i-success-icon">✅</div>
<h2 style="margin-bottom:8px;color:var(--success)">Installation abgeschlossen!</h2>
<p style="color:var(--muted);font-size:.9rem">DeskHub wurde erfolgreich eingerichtet.</p>
<div class="i-action-btns">
<a href="/staff/login.php" class="i-btn-primary">→ Jetzt einloggen</a>
<a href="/login.php" class="i-btn-secondary">Kundenportal</a>
</div>
<hr class="i-divider">
<div class="i-alert warning" style="text-align:left">
<span class="i-alert-icon">🗑️</span>
<span><strong>Wichtig:</strong> Löschen Sie jetzt das Verzeichnis <code>install/</code> vom Server, damit niemand die Installation wiederholen kann.</span>
</div>
</div>
<?php else: ?>
<!-- Step 1: Requirements -->
<div class="i-card">
<div class="i-card-title">
<span class="num">1</span> Systemvoraussetzungen
</div>
<ul class="i-checks">
<?php foreach ($checks as $c):
$cls = $c['ok'] ? 'ok' : (empty($c['optional']) ? 'err' : 'opt');
$ico = $c['ok'] ? '✓' : (empty($c['optional']) ? '✗' : '–');
?>
<li class="i-check-row <?= empty($c['optional']) ? '' : 'optional' ?>">
<span class="i-check-badge <?= $cls ?>"><?= $ico ?></span>
<span><?= h($c['label']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php if (!$requiredOk): ?>
<div class="i-alert error" style="margin-top:16px">
<span class="i-alert-icon">✗</span>
<span>Nicht alle Pflichtvoraussetzungen erfüllt. Bitte die rot markierten Punkte beheben und die Seite neu laden.</span>
</div>
<?php endif; ?>
</div>
<?php if ($requiredOk): ?>
<?php foreach ($errors as $err): ?>
<div class="i-alert error"><span class="i-alert-icon">✗</span><span><?= h($err) ?></span></div>
<?php endforeach; ?>
<!-- Step 2: Configuration form -->
<div class="i-card">
<div class="i-card-title">
<span class="num">2</span> Konfiguration
</div>
<form method="post" id="install-form">
<input type="hidden" name="csrf_token" value="<?= h($_SESSION['install_csrf']) ?>">
<!-- DB driver -->
<div class="i-section-title">🗄️ Datenbank</div>
<div class="i-driver-tabs" id="driver-tabs">
<label class="i-driver-tab <?= ($_POST['driver'] ?? 'mysql') === 'mysql' ? 'active' : '' ?>" id="tab-mysql">
<input type="radio" name="driver" value="mysql" <?= ($_POST['driver'] ?? 'mysql') === 'mysql' ? 'checked' : '' ?>>
<div class="i-driver-tab-label">MySQL / MariaDB</div>
<div class="i-driver-tab-desc">Produktivbetrieb empfohlen</div>
</label>
<label class="i-driver-tab <?= ($_POST['driver'] ?? '') === 'sqlite' ? 'active' : '' ?>" id="tab-sqlite">
<input type="radio" name="driver" value="sqlite" <?= ($_POST['driver'] ?? '') === 'sqlite' ? 'checked' : '' ?>>
<div class="i-driver-tab-label">SQLite</div>
<div class="i-driver-tab-desc">Nur für lokale Tests</div>
</label>
</div>
<div id="mysql-fields" style="display:<?= ($_POST['driver'] ?? 'mysql') === 'sqlite' ? 'none' : 'block' ?>">
<div class="i-row">
<div class="i-field">
<label for="db_host">Host</label>
<input type="text" id="db_host" name="db_host" value="<?= h($_POST['db_host'] ?? 'localhost') ?>" placeholder="localhost">
</div>
<div class="i-field">
<label for="db_name">Datenbankname</label>
<input type="text" id="db_name" name="db_name" value="<?= h($_POST['db_name'] ?? '') ?>" placeholder="deskhub">
</div>
</div>
<div class="i-row">
<div class="i-field">
<label for="db_user">Benutzer</label>
<input type="text" id="db_user" name="db_user" value="<?= h($_POST['db_user'] ?? '') ?>" placeholder="deskhub_user">
</div>
<div class="i-field">
<label for="db_pass">Passwort</label>
<input type="password" id="db_pass" name="db_pass" placeholder="••••••••">
</div>
</div>
<p class="i-hint" style="margin-bottom:16px">Die Datenbank muss bereits existieren (leer) — Tabellen werden automatisch angelegt.</p>
</div>
<!-- Website -->
<div class="i-section-title" style="margin-top:8px">🌐 Website</div>
<div class="i-field">
<label for="site_name">Name der Installation</label>
<input type="text" id="site_name" name="site_name" value="<?= h($_POST['site_name'] ?? 'DeskHub Support') ?>" required>
</div>
<div class="i-field">
<label for="site_url">URL der Installation</label>
<input type="text" id="site_url" name="site_url" value="<?= h($_POST['site_url'] ?? $detectedUrl) ?>" placeholder="https://support.firma.de" required>
<div class="i-hint">Wird in E-Mail-Links verwendet. Kein abschließendes /</div>
</div>
<!-- Admin account -->
<div class="i-section-title" style="margin-top:8px">👤 Administrator-Konto</div>
<div class="i-row">
<div class="i-field">
<label for="admin_name">Name</label>
<input type="text" id="admin_name" name="admin_name" value="<?= h($_POST['admin_name'] ?? '') ?>" placeholder="Max Mustermann" required>
</div>
<div class="i-field">
<label for="admin_email">E-Mail</label>
<input type="email" id="admin_email" name="admin_email" value="<?= h($_POST['admin_email'] ?? '') ?>" placeholder="admin@firma.de" required>
</div>
</div>
<div class="i-row">
<div class="i-field">
<label for="admin_password">Passwort</label>
<input type="password" id="admin_password" name="admin_password" placeholder="••••••••" required>
<div class="i-pw-bar"><div class="i-pw-fill" id="pw-fill"></div></div>
</div>
<div class="i-field">
<label for="admin_password2">Passwort wiederholen</label>
<input type="password" id="admin_password2" name="admin_password2" placeholder="••••••••" required>
</div>
</div>
<button type="submit" class="i-submit-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
Installation starten
</button>
</form>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</main>
</div>
<script>
// Driver tab switching
document.querySelectorAll('.i-driver-tab input[type=radio]').forEach(function(radio) {
radio.addEventListener('change', function() {
document.querySelectorAll('.i-driver-tab').forEach(function(t) { t.classList.remove('active'); });
this.closest('.i-driver-tab').classList.add('active');
document.getElementById('mysql-fields').style.display = this.value === 'mysql' ? 'block' : 'none';
});
});
// Password strength bar
var pwInput = document.getElementById('admin_password');
var pwFill = document.getElementById('pw-fill');
if (pwInput && pwFill) {
pwInput.addEventListener('input', function() {
var v = this.value, score = 0;
if (v.length >= 8) score++;
if (v.length >= 12) score++;
if (/[A-Z]/.test(v)) score++;
if (/[0-9]/.test(v)) score++;
if (/[^A-Za-z0-9]/.test(v)) score++;
var colors = ['#ef4444','#f97316','#eab308','#22c55e','#16a34a'];
var widths = ['20%','40%','60%','80%','100%'];
pwFill.style.width = score > 0 ? widths[score-1] : '0%';
pwFill.style.background = score > 0 ? colors[score-1] : 'transparent';
});
}
</script>
</body>
</html>