feat: Add WFW-Aushang web app with PWA support, offline caching, and dark mode
- Created index.html for the main application interface with responsive design and dark mode support. - Added manifest.webmanifest for PWA configuration, including app icons and display settings. - Implemented service worker (sw.js) for offline caching of assets and network-first strategy for versioning. - Introduced version.json to manage app versioning.
This commit is contained in:
258
index.php
Normal file
258
index.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
// index.php – Basis-Menü + Login mit flexibler Config (YAML/JSON/INI)
|
||||
session_start();
|
||||
|
||||
// ===== Pfade / Basis =====
|
||||
$basePath = __DIR__;
|
||||
$baseUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
|
||||
|
||||
// ===== Config-Lader =====
|
||||
function find_config_path(string $basePath): ?string {
|
||||
// Reihenfolge der Dateitypen: YAML, YML, JSON, INI
|
||||
$candidates = [
|
||||
$basePath . '/config/config.yaml',
|
||||
$basePath . '/config/config.yml',
|
||||
$basePath . '/config/config.json',
|
||||
$basePath . '/config/config.ini',
|
||||
];
|
||||
foreach ($candidates as $c) {
|
||||
if (@is_file($c) && @is_readable($c)) return $c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parse_yaml_with_any(string $path): ?array {
|
||||
// 1) ext-yaml?
|
||||
if (function_exists('yaml_parse_file')) {
|
||||
$cfg = @yaml_parse_file($path);
|
||||
return is_array($cfg) ? $cfg : null;
|
||||
}
|
||||
// 2) Symfony/Yaml über Composer?
|
||||
$autoload = __DIR__ . '/vendor/autoload.php';
|
||||
if (is_file($autoload)) {
|
||||
require_once $autoload;
|
||||
if (class_exists(\Symfony\Component\Yaml\Yaml::class)) {
|
||||
try {
|
||||
$cfg = \Symfony\Component\Yaml\Yaml::parseFile($path);
|
||||
return is_array($cfg) ? $cfg : null;
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function load_config(string $basePath): array {
|
||||
$path = find_config_path($basePath);
|
||||
if (!$path) {
|
||||
http_response_code(500);
|
||||
die("Config-Datei nicht gefunden. Erwartet: config.yaml|yml|json|ini in " . htmlspecialchars($basePath . '/config'));
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$config = null;
|
||||
|
||||
if (in_array($ext, ['yaml', 'yml'], true)) {
|
||||
$config = parse_yaml_with_any($path);
|
||||
if ($config === null) {
|
||||
// Fallback: Versuche JSON/INI wenn vorhanden (z. B. User hat nur eine davon)
|
||||
// Aber hier ist die Datei YAML – wenn nicht parsebar, liefern wir einen klaren Fehler.
|
||||
http_response_code(500);
|
||||
die("YAML-Parser nicht verfügbar oder Datei ungültig: " . htmlspecialchars(basename($path)) . ". Installiere 'ext-yaml' oder 'symfony/yaml', oder nutze JSON/INI.");
|
||||
}
|
||||
} elseif ($ext === 'json') {
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
http_response_code(500);
|
||||
die("Config (JSON) konnte nicht gelesen werden: " . htmlspecialchars(basename($path)));
|
||||
}
|
||||
$config = json_decode($raw, true);
|
||||
if (!is_array($config)) {
|
||||
http_response_code(500);
|
||||
die("Ungültige JSON-Config: " . htmlspecialchars(basename($path)));
|
||||
}
|
||||
} elseif ($ext === 'ini') {
|
||||
$config = @parse_ini_file($path, true, INI_SCANNER_TYPED);
|
||||
if (!is_array($config)) {
|
||||
http_response_code(500);
|
||||
die("Ungültige INI-Config: " . htmlspecialchars(basename($path)));
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
die("Nicht unterstützte Config-Erweiterung: " . htmlspecialchars($ext));
|
||||
}
|
||||
|
||||
// Normalisiere Struktur: wir erwarten auth.user / auth.pass
|
||||
// Erlaube flach (user, pass) ODER verschachtelt (auth[user], auth[pass])
|
||||
$user = $config['auth']['user'] ?? $config['user'] ?? null;
|
||||
$pass = $config['auth']['pass'] ?? $config['pass'] ?? null;
|
||||
|
||||
if (!$user || !$pass) {
|
||||
http_response_code(500);
|
||||
die("Ungültige Config-Struktur. Erwartet z. B.:\n"
|
||||
. "YAML/JSON:\n{\n \"auth\": {\"user\": \"Thomas\", \"pass\": \"Alles4Mich!\"}\n}\n"
|
||||
. "INI:\n[auth]\nuser=Thomas\npass=Alles4Mich!");
|
||||
}
|
||||
|
||||
return ['user' => (string)$user, 'pass' => (string)$pass, 'path' => $path];
|
||||
}
|
||||
|
||||
$cfg = load_config($basePath);
|
||||
$USERNAME = $cfg['user'];
|
||||
$PASSWORD = $cfg['pass'];
|
||||
|
||||
// ===== Login / Logout Handling =====
|
||||
if (isset($_GET['logout'])) {
|
||||
$_SESSION = [];
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time()-42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
|
||||
}
|
||||
session_destroy();
|
||||
header("Location: ".$baseUrl."/");
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
||||
$u = $_POST['user'] ?? '';
|
||||
$p = $_POST['pass'] ?? '';
|
||||
// hash_equals verhindert Timing-Angriffe
|
||||
if (hash_equals($USERNAME, $u) && hash_equals($PASSWORD, $p)) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['auth'] = true;
|
||||
header("Location: ".$baseUrl."/");
|
||||
exit;
|
||||
} else {
|
||||
$errors[] = "Login fehlgeschlagen.";
|
||||
}
|
||||
}
|
||||
|
||||
$authed = ($_SESSION['auth'] ?? false) === true;
|
||||
|
||||
// ===== Verzeichnisse scannen =====
|
||||
$apps = [];
|
||||
if ($authed) {
|
||||
foreach (glob($basePath . '/*', GLOB_ONLYDIR) as $dirPath) {
|
||||
$name = basename($dirPath);
|
||||
|
||||
// Ordner, die NICHT im Menü erscheinen
|
||||
if (in_array($name, ['vendor', '.git', '.well-known', 'config'], true)) continue;
|
||||
|
||||
$iconRel = $name . '/icon.png';
|
||||
$iconAbs = $basePath . '/' . $iconRel;
|
||||
$hasIcon = is_file($iconAbs);
|
||||
|
||||
$apps[] = [
|
||||
'name' => $name,
|
||||
'url' => ($baseUrl ? $baseUrl : '') . '/' . rawurlencode($name) . '/',
|
||||
'icon' => $hasIcon ? (($baseUrl ? $baseUrl : '') . '/' . $iconRel . '?v=' . filemtime($iconAbs)) : null,
|
||||
];
|
||||
}
|
||||
usort($apps, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VIBING – Experimente</title>
|
||||
<style>
|
||||
:root { --bg:#0f1115; --card:#171a21; --fg:#e8eef9; --muted:#9aa4b2; --accent:#7aa2f7; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
||||
background: radial-gradient(1200px 600px at 20% -10%, #182034, transparent), var(--bg); color: var(--fg);
|
||||
min-height: 100svh; display: grid; place-items: center; padding: 24px;
|
||||
}
|
||||
.wrap { width: min(1100px, 100%); }
|
||||
header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; }
|
||||
header h1 { margin:0; font-size: clamp(20px, 3vw, 28px); letter-spacing:.3px; }
|
||||
header a.logout{ color: var(--muted); text-decoration:none; border:1px solid #2a3140; padding:8px 12px; border-radius:10px; }
|
||||
.card {
|
||||
background: linear-gradient(180deg, #1a1f2b, var(--card));
|
||||
border: 1px solid #242b3a; border-radius: 16px; padding: clamp(16px, 3vw, 24px); box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||
}
|
||||
form.login { display:grid; gap:12px; max-width:340px; }
|
||||
label { font-size:14px; color: var(--muted); }
|
||||
input[type=text], input[type=password] {
|
||||
width:100%; padding:12px 14px; border-radius:10px; border:1px solid #2a3140; background:#0f1320; color:var(--fg);
|
||||
}
|
||||
button {
|
||||
padding:12px 14px; border-radius:10px; border:1px solid #2e3650; background: #1f2740; color: var(--fg); cursor:pointer;
|
||||
}
|
||||
.error { color:#ff8b8b; margin:6px 0 0 0; font-size:14px; }
|
||||
|
||||
.grid { display:grid; gap:16px; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||||
.app { display:flex; flex-direction:column; gap:10px; padding:14px; border:1px solid #263147; border-radius:14px; background:#121726; text-decoration:none; color:inherit; }
|
||||
.app:hover { border-color:#2f3c59; transform: translateY(-1px); transition: .15s ease; }
|
||||
.icon {
|
||||
width: 100%; aspect-ratio: 1/1; border-radius: 12px; background: #0d1220; display:grid; place-items:center;
|
||||
overflow:hidden; border:1px solid #232b40;
|
||||
}
|
||||
.icon img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||
.fallback { font-size:38px; color: var(--accent); }
|
||||
.name { font-weight:600; }
|
||||
.empty { color: var(--muted); }
|
||||
footer { margin-top:18px; color: var(--muted); font-size:13px; text-align:center; }
|
||||
.hint { color: var(--muted); font-size: 12px; margin-top:8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>VIBING – Experimente</h1>
|
||||
<?php if ($authed): ?>
|
||||
<a class="logout" href="?logout=1">Logout</a>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<?php if (!$authed): ?>
|
||||
<form class="login" method="post" autocomplete="off">
|
||||
<div>
|
||||
<label for="user">Benutzername</label>
|
||||
<input id="user" name="user" type="text" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pass">Passwort</label>
|
||||
<input id="pass" name="pass" type="password" required>
|
||||
</div>
|
||||
<div>
|
||||
<button name="login" value="1" type="submit">Login</button>
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<div class="error"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endforeach; ?>
|
||||
<div class="hint">
|
||||
Unterstützte Config-Dateien: <code>config.yaml</code>, <code>config.yml</code>, <code>config.json</code>, <code>config.ini</code> im Ordner <code>config/</code>.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<?php if (empty($apps)): ?>
|
||||
<div class="empty">Noch keine Unterverzeichnisse gefunden. Lege Ordner auf Server-Ebene an (z. B. <code>/experiment1</code>) und optional ein <code>icon.png</code> hinein.</div>
|
||||
<?php else: ?>
|
||||
<div class="grid">
|
||||
<?php foreach ($apps as $app): ?>
|
||||
<a class="app" href="<?= htmlspecialchars($app['url'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<div class="icon">
|
||||
<?php if ($app['icon']): ?>
|
||||
<img src="<?= htmlspecialchars($app['icon'], ENT_QUOTES, 'UTF-8') ?>" alt="icon">
|
||||
<?php else: ?>
|
||||
<div class="fallback">🧪</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="name"><?= htmlspecialchars($app['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<footer>Basis-Verzeichnis: <code><?= htmlspecialchars($baseUrl ?: '/', ENT_QUOTES, 'UTF-8') ?></code></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user