Teilt die Bestandsmigration in einen sicheren Request pro Bild auf, zeigt den tatsächlichen Fortschritt im Adminbereich und erlaubt die Wiederaufnahme nach Gateway- oder Netzwerkabbrüchen.
620 lines
26 KiB
PHP
620 lines
26 KiB
PHP
<?php
|
||
// admin.php
|
||
declare(strict_types=1);
|
||
session_start();
|
||
require_once __DIR__ . '/functions.php';
|
||
|
||
$loggedIn = isset($_SESSION['admin']) && $_SESSION['admin'] === true;
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migration_step']) && !$loggedIn) {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Cache-Control: no-store');
|
||
http_response_code(401);
|
||
echo json_encode(['ok' => false, 'error' => 'Die Admin-Sitzung ist abgelaufen. Bitte Seite neu laden und anmelden.']);
|
||
exit;
|
||
}
|
||
|
||
// Login
|
||
if (isset($_POST['action']) && $_POST['action'] === 'login') {
|
||
if (($_POST['password'] ?? '') === ADMIN_PASSWORD_PLAIN) {
|
||
$_SESSION['admin'] = true;
|
||
header('Location: admin.php');
|
||
exit;
|
||
} else {
|
||
$error = 'Falsches Passwort.';
|
||
}
|
||
}
|
||
|
||
// Logout
|
||
if (isset($_GET['logout'])) {
|
||
$_SESSION = [];
|
||
session_destroy();
|
||
header('Location: admin.php');
|
||
exit;
|
||
}
|
||
|
||
if ($loggedIn) {
|
||
if (empty($_SESSION['migration_csrf'])) {
|
||
$_SESSION['migration_csrf'] = bin2hex(random_bytes(32));
|
||
}
|
||
$migrationCsrf = (string)$_SESSION['migration_csrf'];
|
||
$migrationError = null;
|
||
|
||
// Je Request genau ein Bestandsbild migrieren, damit das Frontend echten
|
||
// Fortschritt anzeigen kann und kein langer PHP-Request am Gateway scheitert.
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migration_step'])) {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Cache-Control: no-store');
|
||
$submittedToken = (string)($_POST['migration_csrf'] ?? '');
|
||
if (!hash_equals($migrationCsrf, $submittedToken)) {
|
||
http_response_code(403);
|
||
echo json_encode(['ok' => false, 'error' => 'Die Migrationsanfrage ist abgelaufen. Bitte Seite neu laden.']);
|
||
exit;
|
||
}
|
||
|
||
session_write_close();
|
||
try {
|
||
$result = migrate_next_upload_to_webp();
|
||
echo json_encode(['ok' => true, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||
} catch (Throwable $e) {
|
||
http_response_code(422);
|
||
echo json_encode(
|
||
['ok' => false, 'error' => 'Migration fehlgeschlagen: ' . $e->getMessage()],
|
||
JSON_UNESCAPED_UNICODE
|
||
);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
// CREATE/UPDATE
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_event'])) {
|
||
$events = load_events();
|
||
$id = $_POST['id'] ?: 'evt_' . bin2hex(random_bytes(6));
|
||
$title = sanitize_text($_POST['title'] ?? '');
|
||
$speaker = sanitize_text($_POST['speaker'] ?? '');
|
||
$date = sanitize_text($_POST['date'] ?? '');
|
||
$time = sanitize_text($_POST['time'] ?? '19:30');
|
||
$venue_name = sanitize_text($_POST['venue_name'] ?? '');
|
||
$venue_address = sanitize_text($_POST['venue_address'] ?? '');
|
||
$price = sanitize_text($_POST['price'] ?? '');
|
||
$doors_open = sanitize_text($_POST['doors_open'] ?? '');
|
||
$tickets_info = sanitize_text($_POST['tickets_info'] ?? '');
|
||
$reserve_by_mail = isset($_POST['reserve_by_mail']) && $_POST['reserve_by_mail'] === '1';
|
||
$reserve_email = trim((string)($_POST['reserve_email'] ?? ''));
|
||
if ($reserve_by_mail && $reserve_email === '') {
|
||
$reserve_email = 'walschleber.kultour@gmail.com';
|
||
}
|
||
if ($reserve_email !== '' && !filter_var($reserve_email, FILTER_VALIDATE_EMAIL)) {
|
||
$reserve_email = '';
|
||
}
|
||
$catering = sanitize_text($_POST['catering'] ?? '');
|
||
$tags = sanitize_text($_POST['tags'] ?? '');
|
||
$description = trim($_POST['description'] ?? '');
|
||
$existing_image = sanitize_text($_POST['existing_image'] ?? '');
|
||
|
||
if ($title === '' || $date === '') {
|
||
$error = 'Titel und Datum sind Pflichtfelder.';
|
||
} else {
|
||
try {
|
||
$imagePath = $existing_image;
|
||
if (!empty($_FILES['image']['name'])) {
|
||
$imagePath = handle_image_upload($_FILES['image']);
|
||
}
|
||
$slug = make_slug($title, $date);
|
||
|
||
// Update falls vorhanden
|
||
$events = load_events();
|
||
$idx = null;
|
||
foreach ($events as $k => $ev) {
|
||
if ($ev['id'] === $id) { $idx = $k; break; }
|
||
}
|
||
$record = [
|
||
'id' => $id,
|
||
'title' => $title,
|
||
'speaker' => $speaker,
|
||
'date' => $date,
|
||
'time' => $time,
|
||
'venue_name' => $venue_name,
|
||
'venue_address' => $venue_address,
|
||
'price' => $price,
|
||
'doors_open' => $doors_open,
|
||
'tickets_info' => $tickets_info,
|
||
'reserve_by_mail' => $reserve_by_mail,
|
||
'reserve_email' => $reserve_email,
|
||
'catering' => $catering,
|
||
'tags' => $tags,
|
||
'description' => $description,
|
||
'image' => $imagePath,
|
||
'slug' => $slug,
|
||
'updated_at' => now_iso()
|
||
];
|
||
if ($idx !== null) {
|
||
$events[$idx] = $record;
|
||
} else {
|
||
$record['created_at'] = now_iso();
|
||
$events[] = $record;
|
||
}
|
||
save_events($events);
|
||
header('Location: admin.php?ok=1');
|
||
exit;
|
||
} catch (Throwable $e) {
|
||
$error = 'Fehler: ' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
// DELETE
|
||
if (isset($_GET['delete'])) {
|
||
$events = load_events();
|
||
$id = $_GET['delete'];
|
||
$events = array_values(array_filter($events, fn($e) => $e['id'] !== $id));
|
||
save_events($events);
|
||
header('Location: admin.php?deleted=1');
|
||
exit;
|
||
}
|
||
|
||
// EDIT load
|
||
$editEvent = null;
|
||
if (isset($_GET['edit'])) {
|
||
$id = $_GET['edit'];
|
||
foreach (load_events() as $e) {
|
||
if ($e['id'] === $id) { $editEvent = $e; break; }
|
||
}
|
||
}
|
||
|
||
try {
|
||
$nonWebpUploads = find_non_webp_uploads();
|
||
} catch (Throwable $e) {
|
||
$nonWebpUploads = [];
|
||
$migrationError = 'Upload-Prüfung fehlgeschlagen: ' . $e->getMessage();
|
||
}
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Admin | <?= h(SITE_NAME) ?></title>
|
||
<meta name="robots" content="noindex,nofollow">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="/assets/styles.css">
|
||
<link rel="icon" href="/assets/logo.png" sizes="32x32" type="image/png">
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom sticky-top">
|
||
<div class="container">
|
||
<a class="navbar-brand brand-logo" href="index.html">
|
||
<img src="/assets/logo.png" alt="Walschleber KulTour Logo" height="34" width="34">
|
||
<span><?= h(SITE_NAME) ?></span>
|
||
</a>
|
||
<div class="ms-auto">
|
||
<?php if ($loggedIn): ?>
|
||
<a class="btn btn-outline-secondary btn-sm" href="admin.php?logout=1">Logout</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<?php if (!$loggedIn): ?>
|
||
<header class="py-5 bg-light border-bottom">
|
||
<div class="container">
|
||
<div class="akzent-line mb-3"></div>
|
||
<h1 class="display-6 mb-2">Admin-Login</h1>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container my-5">
|
||
<div class="row justify-content-center">
|
||
<div class="col-md-6 col-lg-5">
|
||
<div class="p-4 border rounded-3 bg-white">
|
||
<?php if (!empty($error)): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||
<form method="post" autocomplete="off">
|
||
<input type="hidden" name="action" value="login">
|
||
<div class="mb-3">
|
||
<label class="form-label">Passwort</label>
|
||
<input type="password" class="form-control form-control-lg" name="password" required>
|
||
</div>
|
||
<button class="btn btn-brand btn-lg w-100">Einloggen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<?php else: ?>
|
||
<header class="py-5 bg-light border-bottom">
|
||
<div class="container">
|
||
<div class="akzent-line mb-3"></div>
|
||
<h1 class="display-6 mb-2">Vorträge verwalten</h1>
|
||
<p class="text-muted mb-0">Anlegen, bearbeiten, Bilder hochladen.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container my-4">
|
||
<div class="d-flex align-items-center gap-2 mb-3">
|
||
<?php if (isset($_GET['ok'])): ?><span class="badge text-bg-success">Gespeichert</span><?php endif; ?>
|
||
<?php if (isset($_GET['deleted'])): ?><span class="badge text-bg-warning">Gelöscht</span><?php endif; ?>
|
||
</div>
|
||
|
||
<?php if (!empty($migrationError)): ?>
|
||
<div class="alert alert-danger" role="alert"><?= h($migrationError) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!empty($nonWebpUploads)): ?>
|
||
<div
|
||
class="alert alert-warning"
|
||
id="webp-migration-panel"
|
||
data-total="<?= count($nonWebpUploads) ?>"
|
||
data-csrf="<?= h($migrationCsrf) ?>"
|
||
role="status"
|
||
>
|
||
<div class="d-md-flex justify-content-between align-items-center gap-3">
|
||
<div class="mb-3 mb-md-0">
|
||
<div class="fw-semibold">WebP-Migration verfügbar</div>
|
||
<div class="small" id="webp-migration-summary">
|
||
<?= count($nonWebpUploads) ?> vorhandene(s) Upload-Bild(er) sind noch nicht als WebP gespeichert.
|
||
Die Veranstaltungseinträge werden automatisch aktualisiert.
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-warning text-nowrap" type="button" id="webp-migration-start">
|
||
In WebP konvertieren
|
||
</button>
|
||
</div>
|
||
<div class="mt-3 d-none" id="webp-migration-progress-wrap">
|
||
<div
|
||
class="progress"
|
||
role="progressbar"
|
||
aria-label="Fortschritt der WebP-Migration"
|
||
aria-valuemin="0"
|
||
aria-valuemax="100"
|
||
aria-valuenow="0"
|
||
>
|
||
<div
|
||
class="progress-bar progress-bar-striped progress-bar-animated"
|
||
id="webp-migration-progress"
|
||
style="width: 0%"
|
||
>0%</div>
|
||
</div>
|
||
<div class="small mt-2" id="webp-migration-status" aria-live="polite">Migration wird vorbereitet …</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="row g-4">
|
||
<div class="col-lg-5">
|
||
<div class="card shadow-sm">
|
||
<div class="card-body">
|
||
<h2 class="h5 mb-3"><?= $editEvent ? 'Vortrag bearbeiten' : 'Neuen Vortrag anlegen' ?></h2>
|
||
<?php if (!empty($error)): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||
|
||
<form method="post" enctype="multipart/form-data">
|
||
<input type="hidden" name="id" value="<?= h($editEvent['id'] ?? '') ?>">
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Titel*</label>
|
||
<input type="text" class="form-control" name="title" required value="<?= h($editEvent['title'] ?? '') ?>">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Referent/Autor</label>
|
||
<input type="text" class="form-control" name="speaker" value="<?= h($editEvent['speaker'] ?? '') ?>">
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Datum*</label>
|
||
<input type="date" class="form-control" name="date" required value="<?= h($editEvent['date'] ?? '') ?>">
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Uhrzeit</label>
|
||
<input type="time" class="form-control" name="time" value="<?= h($editEvent['time'] ?? '19:30') ?>">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Veranstaltungsort (Name)</label>
|
||
<input type="text" class="form-control" name="venue_name" value="<?= h($editEvent['venue_name'] ?? '') ?>">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Adresse (Straße, PLZ Ort)</label>
|
||
<input type="text" class="form-control" name="venue_address" value="<?= h($editEvent['venue_address'] ?? '') ?>">
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Eintritt (z. B. 12 €)</label>
|
||
<input type="text" class="form-control" name="price" value="<?= h($editEvent['price'] ?? '') ?>">
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Einlass (z. B. 18:30)</label>
|
||
<input type="text" class="form-control" name="doors_open" value="<?= h($editEvent['doors_open'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Ticket/VVK-Info</label>
|
||
<input type="text" class="form-control" name="tickets_info" value="<?= h($editEvent['tickets_info'] ?? '') ?>">
|
||
</div>
|
||
|
||
|
||
<div class="form-check form-switch mb-2">
|
||
<input class="form-check-input" type="checkbox" id="reserve_by_mail" name="reserve_by_mail" value="1" <?= !empty($editEvent['reserve_by_mail']) ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="reserve_by_mail">Tickets per E‑Mail reservieren</label>
|
||
</div>
|
||
|
||
<div class="mb-3" id="reserve_email_wrap">
|
||
<label class="form-label">Reservierungs‑E‑Mail (optional)</label>
|
||
<input type="email" class="form-control" name="reserve_email" value="<?= h($editEvent['reserve_email'] ?? '') ?>" placeholder="z.B. tickets@…">
|
||
<div class="form-text">Wenn leer, wird versucht, eine E‑Mail-Adresse aus „Ticket/VVK‑Info“ zu erkennen. Sonst bleibt der Empfänger leer.</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Verpflegung/Service</label>
|
||
<input type="text" class="form-control" name="catering" value="<?= h($editEvent['catering'] ?? '') ?>">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Tags (Kommagetrennt)</label>
|
||
<input type="text" class="form-control" name="tags" value="<?= h($editEvent['tags'] ?? '') ?>">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Beschreibung</label>
|
||
<textarea class="form-control" name="description" rows="6"><?= h($editEvent['description'] ?? '') ?></textarea>
|
||
</div>
|
||
|
||
<?php if (!empty($editEvent['image'])): ?>
|
||
<div class="mb-2">
|
||
<img src="<?= h($editEvent['image']) ?>" alt="aktuelles Bild" class="img-fluid rounded">
|
||
</div>
|
||
<input type="hidden" name="existing_image" value="<?= h($editEvent['image']) ?>">
|
||
<?php endif; ?>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Bild (JPG/PNG/WEBP, wird automatisch als WebP gespeichert)</label>
|
||
<input type="file" class="form-control" name="image" accept=".jpg,.jpeg,.png,.webp">
|
||
</div>
|
||
|
||
<button class="btn btn-brand" name="save_event" value="1">Speichern</button>
|
||
<?php if ($editEvent): ?>
|
||
<a href="admin.php" class="btn btn-outline-secondary ms-2">Abbrechen</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-7">
|
||
<div class="card shadow-sm">
|
||
<div class="card-body">
|
||
<h2 class="h5 mb-3">Kommende Vorträge</h2>
|
||
<?php
|
||
$events = load_events();
|
||
usort($events, 'by_datetime_desc');
|
||
$upcoming = array_values(array_filter($events, 'is_upcoming'));
|
||
$archived = array_values(array_filter($events, fn($e) => !is_upcoming($e)));
|
||
?>
|
||
<?php if (!$upcoming): ?>
|
||
<p class="text-muted">Noch keine Einträge.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Datum</th>
|
||
<th>Titel</th>
|
||
<th>Ort</th>
|
||
<th class="text-end"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($upcoming as $e): ?>
|
||
<tr>
|
||
<td><?= h($e['date']) ?> <?= h($e['time']) ?></td>
|
||
<td>
|
||
<div class="fw-semibold"><?= h($e['title']) ?></div>
|
||
<div class="text-muted small"><?= h($e['speaker']) ?></div>
|
||
<?php if (!empty($e['reserve_by_mail'])): ?>
|
||
<span class="badge text-bg-info">Mail‑Reservierung</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?= h($e['venue_name']) ?></td>
|
||
<td class="text-end">
|
||
<a class="btn btn-sm btn-outline-primary" href="admin.php?edit=<?= h($e['id']) ?>">Bearbeiten</a>
|
||
<a class="btn btn-sm btn-outline-danger" href="admin.php?delete=<?= h($e['id']) ?>" onclick="return confirm('Wirklich löschen?')">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="mt-4">
|
||
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#archive-events" aria-expanded="false" aria-controls="archive-events">
|
||
Archivierte Vorträge (<?= count($archived) ?>)
|
||
</button>
|
||
<div class="collapse mt-3" id="archive-events">
|
||
<?php if (!$archived): ?>
|
||
<p class="text-muted mb-0">Keine archivierten Vorträge vorhanden.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Datum</th>
|
||
<th>Titel</th>
|
||
<th>Ort</th>
|
||
<th class="text-end"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($archived as $e): ?>
|
||
<tr>
|
||
<td><?= h($e['date']) ?> <?= h($e['time']) ?></td>
|
||
<td>
|
||
<div class="fw-semibold"><?= h($e['title']) ?></div>
|
||
<div class="text-muted small"><?= h($e['speaker']) ?></div>
|
||
<?php if (!empty($e['reserve_by_mail'])): ?>
|
||
<span class="badge text-bg-info">Mail‑Reservierung</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?= h($e['venue_name']) ?></td>
|
||
<td class="text-end">
|
||
<a class="btn btn-sm btn-outline-primary" href="admin.php?edit=<?= h($e['id']) ?>">Bearbeiten</a>
|
||
<a class="btn btn-sm btn-outline-danger" href="admin.php?delete=<?= h($e['id']) ?>" onclick="return confirm('Wirklich löschen?')">Löschen</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="my-4">
|
||
<div class="cta-band p-4 d-md-flex justify-content-between align-items-center rounded-4">
|
||
<div class="mb-3 mb-md-0">
|
||
<p class="section-kicker mb-1">Tipp</p>
|
||
<div class="h6 mb-0">Wenn „Ticket/VVK-Info“ eine URL enthält, zeigt die Programmseite automatisch den Button <em>„Tickets sichern“</em>. Wenn „Tickets per E‑Mail reservieren“ aktiv ist, erscheint zusätzlich ein Button, der eine Mailvorlage im Mailprogramm öffnet.</div>
|
||
</div>
|
||
<a class="btn btn-accent" href="programm.php" target="_blank" rel="noopener">Programm ansehen</a>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
</div>
|
||
</main>
|
||
<?php endif; ?>
|
||
|
||
<footer class="py-4 border-top mt-5">
|
||
<div class="container small d-flex gap-3 flex-wrap">
|
||
<span>© <?= date('Y') ?> <?= h(SITE_NAME) ?></span>
|
||
<a href="impressum.html">Impressum</a>
|
||
<a href="datenschutz.html">Datenschutz</a>
|
||
<a class="footer-gitea" href="https://git.mrblake.cc/MrBlake/Walschleber-Kultour-CMS" target="_blank" rel="noopener noreferrer" aria-label="Quellcode auf Gitea ansehen" title="Quellcode auf Gitea">
|
||
<img src="/assets/gitea.svg" width="18" height="18" alt="">
|
||
</a>
|
||
</div>
|
||
</footer>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// Admin UI: Reservierungs‑E‑Mail Feld nur anzeigen, wenn „Tickets per E‑Mail reservieren“ aktiv ist
|
||
(function () {
|
||
const toggle = document.getElementById('reserve_by_mail');
|
||
const wrap = document.getElementById('reserve_email_wrap');
|
||
if (!toggle || !wrap) return;
|
||
const apply = () => { wrap.style.display = toggle.checked ? '' : 'none'; };
|
||
toggle.addEventListener('change', apply);
|
||
apply();
|
||
})();
|
||
|
||
// Bestandsmigration: ein Bild pro Request, damit der Fortschritt sichtbar
|
||
// bleibt und die Migration nach Abbruch gefahrlos fortgesetzt werden kann.
|
||
(function () {
|
||
const panel = document.getElementById('webp-migration-panel');
|
||
const button = document.getElementById('webp-migration-start');
|
||
const wrap = document.getElementById('webp-migration-progress-wrap');
|
||
const progress = document.getElementById('webp-migration-progress');
|
||
const progressBox = progress ? progress.parentElement : null;
|
||
const status = document.getElementById('webp-migration-status');
|
||
const summary = document.getElementById('webp-migration-summary');
|
||
if (!panel || !button || !wrap || !progress || !progressBox || !status || !summary) return;
|
||
|
||
let total = Number.parseInt(panel.dataset.total || '0', 10);
|
||
let completed = 0;
|
||
let running = false;
|
||
|
||
const setProgress = (percent) => {
|
||
const bounded = Math.max(0, Math.min(100, percent));
|
||
progress.style.width = `${bounded}%`;
|
||
progress.textContent = `${bounded}%`;
|
||
progressBox.setAttribute('aria-valuenow', String(bounded));
|
||
};
|
||
|
||
const finish = () => {
|
||
setProgress(100);
|
||
progress.classList.remove('progress-bar-animated');
|
||
panel.classList.remove('alert-warning', 'alert-danger');
|
||
panel.classList.add('alert-success');
|
||
summary.textContent = 'WebP-Migration abgeschlossen.';
|
||
status.textContent = 'Alle Bestandsbilder wurden konvertiert und ihre Datenbank-Verweise aktualisiert.';
|
||
button.classList.add('d-none');
|
||
running = false;
|
||
};
|
||
|
||
const runMigration = async () => {
|
||
if (running) return;
|
||
if (completed === 0 && !window.confirm('Alle erkannten Bestandsbilder jetzt nach WebP konvertieren?')) return;
|
||
|
||
running = true;
|
||
button.disabled = true;
|
||
button.textContent = 'Migration läuft …';
|
||
wrap.classList.remove('d-none');
|
||
panel.classList.remove('alert-danger');
|
||
panel.classList.add('alert-warning');
|
||
progress.classList.add('progress-bar-animated');
|
||
|
||
try {
|
||
while (true) {
|
||
status.textContent = `Konvertiere Bild ${Math.min(completed + 1, total)} von ${total} …`;
|
||
const body = new URLSearchParams({
|
||
migration_step: '1',
|
||
migration_csrf: panel.dataset.csrf || ''
|
||
});
|
||
const response = await fetch('admin.php', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
|
||
body: body.toString()
|
||
});
|
||
const responseText = await response.text();
|
||
let payload;
|
||
try {
|
||
payload = JSON.parse(responseText);
|
||
} catch (error) {
|
||
throw new Error(`Ungültige Serverantwort (HTTP ${response.status}).`);
|
||
}
|
||
if (!response.ok || !payload.ok) {
|
||
throw new Error(payload.error || `Serverfehler (HTTP ${response.status}).`);
|
||
}
|
||
|
||
const result = payload.result;
|
||
if (result.processed === 0 || result.remaining_count === 0) {
|
||
finish();
|
||
return;
|
||
}
|
||
|
||
completed += result.processed;
|
||
total = Math.max(total, completed + result.remaining_count);
|
||
setProgress(Math.round((completed / total) * 100));
|
||
status.textContent = `${result.source} wurde konvertiert. Noch ${result.remaining_count} Bild(er).`;
|
||
|
||
if (result.deletion_failed) {
|
||
throw new Error(`Die Originaldatei ${result.source} konnte nicht entfernt werden.`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
running = false;
|
||
button.disabled = false;
|
||
button.textContent = 'Migration fortsetzen';
|
||
progress.classList.remove('progress-bar-animated');
|
||
panel.classList.remove('alert-warning');
|
||
panel.classList.add('alert-danger');
|
||
summary.textContent = 'Die Migration wurde unterbrochen.';
|
||
status.textContent = error instanceof Error ? error.message : 'Unbekannter Fehler.';
|
||
}
|
||
};
|
||
|
||
button.addEventListener('click', runMigration);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|