Add show management and going-live controls to admin

- admin/shows.php: list shows
- admin/show_edit.php: add, edit and delete shows, with validated
  background image uploads saved under generated names
- admin/index.php: queue the next show, switch the live show, or clear
  the queue
- includes/broadcasts.php: setBroadcastShow() and clearQueuedShow(),
  shared by the dashboard and cli/set_show.php
- includes/admin_layout.php: flash messages and Shows nav link

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel MesserandClaude Opus 5.5 committed 2026-10-29 13:48:14 -05:00
1 parent babb71660c
commit 6e61fc6bda
7 files changed
+297 -12

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
<?php
require __DIR__ . '/../includes/auth.php';
require __DIR__ . '/../includes/admin_layout.php';
$admin = requireAdmin();
$db = getDB();
const BACKGROUND_DIR = __DIR__ . '/../images/backgrounds/';
const MAX_BACKGROUND_BYTES = 5 * 1024 * 1024;
// Uploads are identified by their content, not their filename, and saved under a generated name
const BACKGROUND_TYPES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
function slugify(string $name): string {
$slug = strtolower(str_replace(["'", '’'], '', $name));
return trim(preg_replace('/[^a-z0-9]+/', '-', $slug), '-');
}
function deleteBackground(?string $filename): void {
if ($filename && is_file(BACKGROUND_DIR . basename($filename))) {
unlink(BACKGROUND_DIR . basename($filename));
}
}
$id = (int)($_GET['id'] ?? 0);
$show = ['id' => 0, 'name' => '', 'slug' => '', 'background_image' => null];
if ($id) {
$stmt = $db->prepare('SELECT id, name, slug, background_image FROM shows WHERE id = ?');
$stmt->execute([$id]);
$show = $stmt->fetch();
if (!$show) {
http_response_code(404);
exit('Show not found.');
}
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// PHP discards the whole form when a request exceeds post_max_size, so catch that before the CSRF check
if (empty($_POST) && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
$errors[] = 'That upload is too large. Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB.';
} else {
verifyCsrf();
if (($_POST['action'] ?? '') === 'delete' && $id) {
$db->prepare('DELETE FROM shows WHERE id = ?')->execute([$id]);
deleteBackground($show['background_image']);
flash("Deleted {$show['name']}.");
header('Location: shows.php');
exit;
}
$show['name'] = trim((string)($_POST['name'] ?? ''));
$show['slug'] = trim((string)($_POST['slug'] ?? '')) ?: slugify($show['name']);
if ($show['name'] === '' || mb_strlen($show['name']) > 255) {
$errors[] = 'Name is required (up to 255 characters).';
}
if (!preg_match('/^[a-z0-9]+(-[a-z0-9]+)*$/', $show['slug']) || strlen($show['slug']) > 64) {
$errors[] = 'Slug may only contain lowercase letters, numbers and single dashes (up to 64 characters).';
} else {
$stmt = $db->prepare('SELECT COUNT(*) FROM shows WHERE slug = ? AND id != ?');
$stmt->execute([$show['slug'], $id]);
if ($stmt->fetchColumn() > 0) $errors[] = "Another show already uses the slug '{$show['slug']}'.";
}
$upload = $_FILES['background'] ?? null;
$uploadExt = null;
if ($upload && $upload['error'] !== UPLOAD_ERR_NO_FILE) {
if (in_array($upload['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) || $upload['size'] > MAX_BACKGROUND_BYTES) {
$errors[] = 'Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB.';
} elseif ($upload['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'The background image failed to upload. Please try again.';
} else {
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($upload['tmp_name']);
$uploadExt = BACKGROUND_TYPES[$mime] ?? null;
if ($uploadExt === null || getimagesize($upload['tmp_name']) === false) {
$errors[] = 'Background images must be JPEG, PNG or WebP.';
}
}
}
if (!$errors) {
$oldBackground = $show['background_image'];
if ($uploadExt !== null) {
$filename = $show['slug'] . '-' . bin2hex(random_bytes(4)) . '.' . $uploadExt;
if (move_uploaded_file($upload['tmp_name'], BACKGROUND_DIR . $filename)) {
$show['background_image'] = $filename;
} else {
$errors[] = 'The background image could not be saved. Check that the web server can write to images/backgrounds/.';
}
} elseif (!empty($_POST['remove_background'])) {
$show['background_image'] = null;
}
}
if (!$errors) {
if ($id) {
$db->prepare('UPDATE shows SET name = ?, slug = ?, background_image = ? WHERE id = ?')
->execute([$show['name'], $show['slug'], $show['background_image'], $id]);
} else {
$db->prepare('INSERT INTO shows (name, slug, background_image) VALUES (?, ?, ?)')
->execute([$show['name'], $show['slug'], $show['background_image']]);
}
if ($oldBackground !== $show['background_image']) deleteBackground($oldBackground);
flash("Saved {$show['name']}.");
header('Location: shows.php');
exit;
}
}
}
adminHeader($id ? 'Edit Show' : 'Add a Show', $admin);
?>
<?php foreach ($errors as $error): ?>
<p class="admin-message error"><?= e($error) ?></p>
<?php endforeach; ?>
<form method="post" enctype="multipart/form-data" action="show_edit.php<?= $id ? '?id=' . $id : '' ?>">
<?= csrfField() ?>
<input type="hidden" name="MAX_FILE_SIZE" value="<?= MAX_BACKGROUND_BYTES ?>" />
<div class="row gtr-uniform gtr-50">
<div class="col-12">
<label for="name">Name</label>
<input type="text" name="name" id="name" value="<?= e($show['name']) ?>" maxlength="255" required />
</div>
<div class="col-12">
<label for="slug">Slug</label>
<input type="text" name="slug" id="slug" value="<?= e($show['slug']) ?>" maxlength="64" pattern="[a-z0-9]+(-[a-z0-9]+)*" placeholder="Leave blank to generate from the name" />
<p><small>Short name used by <code>cli/set_show.php</code>. Lowercase letters, numbers and dashes.</small></p>
</div>
<div class="col-12">
<label for="background">Background image</label>
<?php if ($show['background_image']): ?>
<p><img src="../images/backgrounds/<?= e(rawurlencode($show['background_image'])) ?>" alt="Current background" style="max-width:320px;border-radius:4px;display:block;"></p>
<?php endif; ?>
<input type="file" name="background" id="background" accept="image/jpeg,image/png,image/webp" />
<p><small>JPEG, PNG or WebP, up to <?= MAX_BACKGROUND_BYTES / 1024 / 1024 ?> MB. Shown behind the player on the live page during this show.</small></p>
<?php if ($show['background_image']): ?>
<input type="checkbox" name="remove_background" id="remove_background" value="1" />
<label for="remove_background">Remove background (use the default)</label>
<?php endif; ?>
</div>
<div class="col-12">
<ul class="actions">
<li><input type="submit" class="primary" value="Save" /></li>
<li><a href="shows.php" class="button">Cancel</a></li>
</ul>
</div>
</div>
</form>
<?php if ($id): ?>
<hr />
<form method="post" action="show_edit.php?id=<?= $id ?>" onsubmit="return confirm('Delete <?= e(addslashes($show['name'])) ?>? This cannot be undone.');">
<?= csrfField() ?>
<input type="hidden" name="action" value="delete" />
<input type="submit" class="small" value="Delete this show" />
</form>
<?php endif; ?>
<?php
adminFooter();