Files
vibing/index.php
thrhymes b049dded72 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.
2025-12-24 16:59:51 +01:00

259 lines
10 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
// 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>