Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

13 changed files with 124 additions and 1646 deletions

View File

@ -1,164 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/app.php';
// Set response header
header('Content-Type: application/json');
function verify_signature($publicKey): bool {
$signature = $_SERVER['HTTP_X_SIGNATURE_ED25519'] ?? null;
$timestamp = $_SERVER['HTTP_X_SIGNATURE_TIMESTAMP'] ?? null;
$body = file_get_contents('php://input');
if (!$signature || !$timestamp || !$body) {
return false;
}
$message = $timestamp . $body;
try {
return sodium_crypto_sign_verify_detached(
hex2bin($signature),
$message,
hex2bin($publicKey)
);
} catch (Exception $e) {
return false;
}
}
$settings = get_settings();
$publicKey = $settings['discord_public_key'] ?? '';
if (empty($publicKey)) {
http_response_code(500);
echo json_encode(['error' => 'Public key not configured']);
exit;
}
if (!verify_signature($publicKey)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid request signature']);
exit;
}
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Type 1: PING
if ($data['type'] === 1) {
echo json_encode(['type' => 1]);
exit;
}
// Type 2: APPLICATION_COMMAND
if ($data['type'] === 2) {
$commandName = $data['data']['name'];
$user = $data['member']['user']['username'] ?? $data['user']['username'] ?? 'Unknown';
$userId = $data['member']['user']['id'] ?? $data['user']['id'] ?? '0';
$guildId = $data['guild_id'] ?? 'DM';
$userRoles = $data['member']['roles'] ?? [];
// Permission check removed as per user request (no DJ role needed)
switch ($commandName) {
case 'play':
$query = '';
foreach ($data['data']['options'] as $opt) {
if ($opt['name'] === 'query') {
$query = $opt['value'];
break;
}
}
if (empty($query)) {
echo json_encode([
'type' => 4,
'data' => ['content' => 'Please provide a song query.']
]);
exit;
}
// Add to database queue
$pdo = db();
$stmt = $pdo->prepare('INSERT INTO music_requests (guild_name, requester_name, query_text, source_type, status) VALUES (:guild, :user, :query, :source, "queued")');
$stmt->execute([
':guild' => $guildId,
':user' => $user,
':query' => $query,
':source' => 'discord_slash'
]);
add_log($user, 'slash_command', "Played: $query", $guildId);
echo json_encode([
'type' => 4,
'data' => ['content' => "✅ Added to queue: **$query**"]
]);
break;
case 'queue':
$pdo = db();
$requests = $pdo->query('SELECT query_text, status FROM music_requests WHERE status IN ("queued", "playing") ORDER BY id ASC LIMIT 5')->fetchAll();
if (empty($requests)) {
$content = "The queue is currently empty.";
} else {
$content = "🎶 **Current Queue:**\n";
foreach ($requests as $index => $req) {
$prefix = ($req['status'] === 'playing') ? "▶️" : ($index + 1) . ".";
$content .= "$prefix {$req['query_text']} ({$req['status']})\n";
}
}
echo json_encode([
'type' => 4,
'data' => ['content' => $content]
]);
break;
case 'skip':
$pdo = db();
$stmt = $pdo->prepare('UPDATE music_requests SET status = "skipped" WHERE status = "playing" LIMIT 1');
$stmt->execute();
if ($stmt->rowCount() > 0) {
$msg = "⏭️ Skipped the current song.";
add_log($user, 'slash_command', "Skipped current song", $guildId);
} else {
$msg = "There is no song currently playing to skip.";
}
echo json_encode([
'type' => 4,
'data' => ['content' => $msg]
]);
break;
case 'stop':
$pdo = db();
$pdo->exec('UPDATE music_requests SET status = "failed" WHERE status IN ("queued", "playing")');
add_log($user, 'slash_command', "Stopped and cleared queue", $guildId);
echo json_encode([
'type' => 4,
'data' => ['content' => "🛑 Music stopped and queue cleared."]
]);
break;
default:
echo json_encode([
'type' => 4,
'data' => ['content' => "Unknown command: $commandName"]
]);
break;
}
}

View File

@ -1,223 +0,0 @@
:root {
color-scheme: light;
--bg: #f5f6f8;
--surface: #ffffff;
--surface-muted: #f0f2f5;
--border: #e3e6ea;
--text: #111827;
--muted: #6b7280;
--accent: #0f172a;
--accent-soft: #e2e8f0;
--success: #166534;
--warning: #b45309;
--danger: #b91c1c;
--radius-sm: 6px;
--radius-md: 10px;
--shadow-sm: 0 8px 18px rgba(15, 23, 42, 0.08);
}
body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.app-shell {
min-height: calc(100vh - 140px);
}
.app-navbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.navbar-brand {
font-weight: 600;
letter-spacing: -0.2px;
}
.nav-link {
color: var(--muted);
font-weight: 500;
}
.nav-link.active,
.nav-link:hover {
color: var(--text);
}
.app-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.app-card .card-body {
padding: 1.5rem;
}
.btn-primary {
background: var(--accent);
border-color: var(--accent);
border-radius: var(--radius-sm);
padding: 0.55rem 1.1rem;
}
.btn-outline-secondary,
.btn-outline-secondary:hover {
border-radius: var(--radius-sm);
}
.btn-link {
color: var(--text);
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.75rem;
color: var(--muted);
}
.status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.status-grid .label {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.25rem;
letter-spacing: 0.08em;
}
.status-grid .value {
font-weight: 600;
margin-bottom: 0;
}
.command-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
font-size: 0.85rem;
}
.command-grid span {
background: var(--surface-muted);
border-radius: var(--radius-sm);
padding: 0.35rem 0.5rem;
text-align: center;
border: 1px solid var(--border);
}
.empty-state {
border: 1px dashed var(--border);
padding: 1.25rem;
border-radius: var(--radius-md);
background: var(--surface-muted);
}
.status-badge {
border-radius: 999px;
padding: 0.35rem 0.65rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.status-queued {
background: #e2e8f0;
color: #1f2937;
}
.status-playing {
background: #dcfce7;
color: var(--success);
}
.status-paused {
background: #fef3c7;
color: var(--warning);
}
.status-skipped,
.status-ended {
background: #e5e7eb;
color: #374151;
}
.status-failed {
background: #fee2e2;
color: var(--danger);
}
.list-group-item {
border-color: var(--border);
}
.app-footer {
border-top: 1px solid var(--border);
background: var(--surface);
}
.form-control,
.form-select {
border-radius: var(--radius-sm);
border-color: var(--border);
}
.form-control:focus,
.form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 0.2rem rgba(15, 23, 42, 0.1);
}
.table {
margin-bottom: 0;
}
.table thead th {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.toast {
border-radius: var(--radius-md);
border-color: var(--border);
box-shadow: var(--shadow-sm);
}
.detail-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.detail-meta .label {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--muted);
letter-spacing: 0.08em;
margin-bottom: 0.25rem;
}
.detail-meta .value {
margin-bottom: 0;
font-weight: 600;
}
@media (max-width: 768px) {
.status-grid,
.detail-meta {
grid-template-columns: 1fr;
}
.command-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@ -1,6 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.toast').forEach((toastEl) => {
const toast = new bootstrap.Toast(toastEl, { delay: 4000 });
toast.show();
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

View File

@ -1,3 +0,0 @@
[*] Discord Bot Worker started...
[+] Processing request #1: 'Bawa dia kembali' for server 'LAST XPERIENCE STUDIO'
[#] Request #1 completed.

View File

@ -1,64 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
function register_commands() {
$settings = db()->query("SELECT discord_token, discord_app_id FROM bot_settings LIMIT 1")->fetch();
if (!$settings || empty($settings['discord_token']) || empty($settings['discord_app_id'])) {
return "Error: Discord credentials not set in settings.";
}
$token = $settings['discord_token'];
$appId = $settings['discord_app_id'];
$url = "https://discord.com/api/v10/applications/$appId/commands";
$commands = [
[
"name" => "play",
"description" => "Play a song from a URL or search term",
"options" => [
[
"name" => "query",
"description" => "The song URL or name to search for",
"type" => 3, // STRING
"required" => true
]
]
],
[
"name" => "queue",
"description" => "Show the current music queue"
],
[
"name" => "skip",
"description" => "Skip the currently playing song"
],
[
"name" => "stop",
"description" => "Stop the music and clear the queue"
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($commands));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bot $token",
"Content-Type: application/json"
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 200 && $status < 300) {
return "Successfully registered " . count($commands) . " slash commands.";
} else {
return "Failed to register commands. Status: $status. Response: $response";
}
}
if (php_sapi_name() === 'cli') {
echo register_commands() . PHP_EOL;
}

View File

@ -1,75 +0,0 @@
<?php
/**
* Discord Bot Worker Script
* This script runs in the background and processes music requests.
* In a real-world scenario, this would use a library like discord-php
* to maintain a persistent WebSocket connection to Discord.
*/
declare(strict_types=1);
// Ensure we are running from CLI
if (PHP_SAPI !== 'cli') {
die("This script must be run from the command line.\n");
}
require_once __DIR__ . '/../includes/app.php';
echo "[*] Discord Bot Worker started...\n";
add_log('System', 'Worker Started', 'The background bot worker process has initiated.');
$settings = get_settings();
if (empty($settings['discord_token'])) {
echo "[!] WARNING: No Discord Token found in settings. Please add it via the dashboard.\n";
add_log('System', 'Worker Warning', 'Discord Token is missing. Worker is running in simulation mode.');
}
// Keep the process alive
while (true) {
try {
$pdo = db();
// 1. Check for queued requests
$stmt = $pdo->prepare('SELECT * FROM music_requests WHERE status = "queued" ORDER BY created_at ASC LIMIT 1');
$stmt->execute();
$request = $stmt->fetch();
if ($request) {
$id = $request['id'];
$query = $request['query_text'];
$guild = $request['guild_name'];
echo "[+] Processing request #{$id}: '{$query}' for server '{$guild}'\n";
// Start "playing"
$update = $pdo->prepare('UPDATE music_requests SET status = "playing" WHERE id = ?');
$update->execute([$id]);
add_log('Bot', 'Playing', "Now playing: {$query}", $guild);
// Simulation: Wait 10 seconds (in a real bot, this would wait for the audio to finish)
sleep(10);
// Mark as "ended"
$update = $pdo->prepare('UPDATE music_requests SET status = "ended" WHERE id = ?');
$update->execute([$id]);
add_log('Bot', 'Finished', "Finished playing: {$query}", $guild);
echo "[#] Request #{$id} completed.\n";
}
// 2. Refresh settings occasionally (every 30 seconds)
static $last_refresh = 0;
if (time() - $last_refresh > 30) {
$settings = get_settings();
$last_refresh = time();
}
// Sleep to avoid high CPU usage
sleep(2);
} catch (Exception $e) {
echo "[!] ERROR: " . $e->getMessage() . "\n";
add_log('System', 'Worker Error', $e->getMessage());
sleep(10); // Wait longer on error before retry
}
}

View File

@ -1,120 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
function h(string $value): string {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function ensure_tables(): void {
$pdo = db();
$pdo->exec(
'CREATE TABLE IF NOT EXISTS music_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
guild_name VARCHAR(120) NOT NULL,
requester_name VARCHAR(80) NOT NULL,
query_text VARCHAR(255) NOT NULL,
source_type VARCHAR(20) NOT NULL,
voice_channel VARCHAR(80) DEFAULT NULL,
notes VARCHAR(255) DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT "queued",
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS bot_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(100) DEFAULT NULL,
user_name VARCHAR(100) NOT NULL,
action VARCHAR(50) NOT NULL,
details TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS bot_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
prefix VARCHAR(8) NOT NULL,
dj_role VARCHAR(80) DEFAULT NULL,
max_volume INT NOT NULL DEFAULT 100,
auto_reconnect TINYINT(1) NOT NULL DEFAULT 1,
log_level VARCHAR(16) NOT NULL DEFAULT "info",
discord_token VARCHAR(255) DEFAULT NULL,
discord_app_id VARCHAR(100) DEFAULT NULL,
discord_public_key VARCHAR(100) DEFAULT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
);
// Add missing columns if they don't exist
$cols = $pdo->query("SHOW COLUMNS FROM bot_settings")->fetchAll(PDO::FETCH_COLUMN);
if (!in_array('discord_token', $cols)) {
$pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_token VARCHAR(255) DEFAULT NULL AFTER log_level');
}
if (!in_array('discord_app_id', $cols)) {
$pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_app_id VARCHAR(100) DEFAULT NULL AFTER discord_token');
}
if (!in_array('discord_public_key', $cols)) {
$pdo->exec('ALTER TABLE bot_settings ADD COLUMN discord_public_key VARCHAR(100) DEFAULT NULL AFTER discord_app_id');
}
$count = (int)$pdo->query('SELECT COUNT(*) FROM bot_settings')->fetchColumn();
if ($count === 0) {
$stmt = $pdo->prepare('INSERT INTO bot_settings (prefix, dj_role, max_volume, auto_reconnect, log_level) VALUES (:prefix, :dj_role, :max_volume, :auto_reconnect, :log_level)');
$stmt->execute([
':prefix' => '!',
':dj_role' => 'DJ',
':max_volume' => 100,
':auto_reconnect' => 1,
':log_level' => 'info',
]);
}
}
function get_settings(): array {
$pdo = db();
$settings = $pdo->query('SELECT * FROM bot_settings ORDER BY id ASC LIMIT 1')->fetch();
if (!$settings) {
ensure_tables();
$settings = $pdo->query('SELECT * FROM bot_settings ORDER BY id ASC LIMIT 1')->fetch();
}
return $settings ?: [
'id' => 0,
'prefix' => '!',
'dj_role' => 'DJ',
'max_volume' => 100,
'auto_reconnect' => 1,
'log_level' => 'info',
'discord_token' => '',
'discord_app_id' => '',
'discord_public_key' => '',
];
}
function status_badge_class(string $status): string {
$map = [
'queued' => 'status-queued',
'playing' => 'status-playing',
'paused' => 'status-paused',
'skipped' => 'status-skipped',
'ended' => 'status-ended',
'failed' => 'status-failed',
];
return $map[$status] ?? 'status-queued';
}
function add_log(string $user, string $action, ?string $details = null, ?string $guild_id = null): void {
$pdo = db();
$stmt = $pdo->prepare('INSERT INTO bot_logs (user_name, action, details, guild_id) VALUES (:user, :action, :details, :guild_id)');
$stmt->execute([
':user' => $user,
':action' => $action,
':details' => $details,
':guild_id' => $guild_id,
]);
}

410
index.php
View File

@ -4,309 +4,147 @@ declare(strict_types=1);
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$settings = get_settings();
$formErrors = [];
$values = [
'guild_name' => '',
'requester_name' => '',
'query_text' => '',
'source_type' => 'search',
'voice_channel' => '',
'notes' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_request') {
$values['guild_name'] = trim((string)($_POST['guild_name'] ?? ''));
$values['requester_name'] = trim((string)($_POST['requester_name'] ?? ''));
$values['query_text'] = trim((string)($_POST['query_text'] ?? ''));
$values['source_type'] = trim((string)($_POST['source_type'] ?? 'search'));
$values['voice_channel'] = trim((string)($_POST['voice_channel'] ?? ''));
$values['notes'] = trim((string)($_POST['notes'] ?? ''));
if ($values['guild_name'] === '') {
$formErrors[] = 'Server/Guild name is required.';
}
if ($values['requester_name'] === '') {
$formErrors[] = 'Requester name is required.';
}
if ($values['query_text'] === '') {
$formErrors[] = 'Song title or URL is required.';
}
if (!in_array($values['source_type'], ['search', 'url'], true)) {
$formErrors[] = 'Source type is invalid.';
}
if (!$formErrors) {
$stmt = db()->prepare('INSERT INTO music_requests (guild_name, requester_name, query_text, source_type, voice_channel, notes, status) VALUES (:guild_name, :requester_name, :query_text, :source_type, :voice_channel, :notes, :status)');
$stmt->execute([
':guild_name' => $values['guild_name'],
':requester_name' => $values['requester_name'],
':query_text' => $values['query_text'],
':source_type' => $values['source_type'],
':voice_channel' => $values['voice_channel'],
':notes' => $values['notes'],
':status' => 'queued',
]);
$newId = (int)db()->lastInsertId();
header('Location: request.php?id=' . $newId . '&created=1');
exit;
}
}
$recentRequests = db()->query('SELECT id, guild_name, requester_name, query_text, source_type, status, created_at FROM music_requests ORDER BY created_at DESC LIMIT 6')->fetchAll();
$recentLogs = db()->query('SELECT user_name, action, created_at FROM bot_logs ORDER BY created_at DESC LIMIT 5')->fetchAll();
if (!$recentLogs) {
add_log('System', 'INITIALIZE', 'CMS Dashboard initialized successfully.');
add_log('Admin', 'CONFIG_UPDATE', 'Bot settings updated (prefix and volume).');
$recentLogs = db()->query('SELECT user_name, action, created_at FROM bot_logs ORDER BY created_at DESC LIMIT 5')->fetchAll();
}
// Check if worker is running
$workerPid = shell_exec("pgrep -f 'bot/worker.php'");
$isWorkerRunning = !empty($workerPid);
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($projectName) ?></title>
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
<div class="container">
<a class="navbar-brand" href="index.php">EchoLift</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto">
<a class="nav-link active" href="index.php">Dashboard</a>
<a class="nav-link" href="requests.php">Queue</a>
<a class="nav-link" href="logs.php">Logs</a>
<a class="nav-link" href="settings.php">Settings</a>
</div>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</nav>
<main class="app-shell">
<section class="container py-4 py-lg-5">
<div class="row align-items-center gy-4">
<div class="col-lg-7">
<p class="eyebrow">Stable Discord music operations</p>
<h1 class="display-5">Manage play requests, queue health, and bot settings in one restrained console.</h1>
<p class="lead text-muted">Submit a song by URL or search query, track status per server, and keep moderation settings consistent across communities.</p>
<div class="d-flex gap-2">
<a class="btn btn-primary" href="#request-form">Create a request</a>
<a class="btn btn-outline-secondary" href="requests.php">View queue</a>
</div>
</div>
<div class="col-lg-5">
<div class="card app-card">
<div class="card-body">
<h2 class="h5">Bot status</h2>
<div class="status-grid">
<div>
<p class="label">Auto-reconnect</p>
<p class="value"><?= $settings['auto_reconnect'] ? 'Enabled' : 'Disabled' ?></p>
</div>
<div>
<p class="label">Command prefix</p>
<p class="value"><?= h($settings['prefix']) ?></p>
</div>
<div>
<p class="label">Max volume</p>
<p class="value"><?= h((string)$settings['max_volume']) ?>%</p>
</div>
<div>
<p class="label">Log level</p>
<p class="value"><?= h($settings['log_level']) ?></p>
</div>
<div>
<p class="label">Worker Status</p>
<p class="value <?= $isWorkerRunning ? 'text-success' : 'text-danger' ?>">
<?= $isWorkerRunning ? 'Always-on' : 'Stopped' ?>
</p>
</div>
<div>
<p class="label">Discord link</p>
<p class="value <?= $settings['discord_token'] ? 'text-success' : 'text-danger' ?>">
<?= $settings['discord_token'] ? 'Connected' : 'Missing token' ?>
</p>
</div>
</div>
<a class="btn btn-sm btn-outline-secondary mt-3" href="settings.php">Adjust settings</a>
</div>
</div>
</div>
</div>
</section>
<section class="container pb-5">
<div class="row g-4">
<div class="col-lg-7">
<div class="card app-card" id="request-form">
<div class="card-body">
<h2 class="h5">New play request</h2>
<p class="text-muted">Create a queue item that the bot will pick up and process in the voice channel.</p>
<?php if ($formErrors): ?>
<div class="alert alert-warning" role="alert">
<?= h(implode(' ', $formErrors)) ?>
</div>
<?php endif; ?>
<form method="post" action="index.php" class="row g-3">
<input type="hidden" name="action" value="create_request">
<div class="col-md-6">
<label class="form-label" for="guild_name">Server / Guild</label>
<input class="form-control" id="guild_name" name="guild_name" required value="<?= h($values['guild_name']) ?>" placeholder="Aurora Community">
</div>
<div class="col-md-6">
<label class="form-label" for="requester_name">Requester</label>
<input class="form-control" id="requester_name" name="requester_name" required value="<?= h($values['requester_name']) ?>" placeholder="@luna">
</div>
<div class="col-md-8">
<label class="form-label" for="query_text">Song title or URL</label>
<input class="form-control" id="query_text" name="query_text" required value="<?= h($values['query_text']) ?>" placeholder="Search title or paste URL">
</div>
<div class="col-md-4">
<label class="form-label" for="source_type">Source</label>
<select class="form-select" id="source_type" name="source_type">
<option value="search" <?= $values['source_type'] === 'search' ? 'selected' : '' ?>>Search</option>
<option value="url" <?= $values['source_type'] === 'url' ? 'selected' : '' ?>>Direct URL</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="voice_channel">Voice channel</label>
<input class="form-control" id="voice_channel" name="voice_channel" value="<?= h($values['voice_channel']) ?>" placeholder="#lofi-room">
</div>
<div class="col-md-6">
<label class="form-label" for="notes">Notes</label>
<input class="form-control" id="notes" name="notes" value="<?= h($values['notes']) ?>" placeholder="Priority, mood, or DJ note">
</div>
<div class="col-12 d-flex gap-2">
<button class="btn btn-primary" type="submit">Submit to queue</button>
<button class="btn btn-outline-secondary" type="reset">Clear</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card app-card">
<div class="card-body">
<h2 class="h5">Recent queue activity</h2>
<p class="text-muted">Latest submissions across all servers.</p>
<?php if (!$recentRequests): ?>
<div class="empty-state">
<p class="mb-1">No requests yet.</p>
<p class="text-muted small">Submit the first play request to start the queue.</p>
</div>
<?php else: ?>
<div class="list-group list-group-flush">
<?php foreach ($recentRequests as $request): ?>
<a class="list-group-item list-group-item-action" href="request.php?id=<?= (int)$request['id'] ?>">
<div class="d-flex justify-content-between">
<div>
<p class="mb-1 fw-semibold"><?= h($request['query_text']) ?></p>
<p class="mb-0 small text-muted"><?= h($request['guild_name']) ?> • <?= h($request['requester_name']) ?></p>
</div>
<span class="badge status-badge status-<?= h($request['status']) ?>"><?= h(ucfirst($request['status'])) ?></span>
</div>
<p class="small text-muted mt-2 mb-0"><?= h($request['source_type']) ?> • <?= h($request['created_at']) ?></p>
</a>
<?php endforeach; ?>
</div>
<a class="btn btn-link mt-3 px-0" href="requests.php">Open full queue</a>
<?php endif; ?>
</div>
</div>
<div class="card app-card mt-4">
<div class="card-body">
<h2 class="h5">Recent audit logs</h2>
<p class="text-muted">Security and operational events.</p>
<?php if (!$recentLogs): ?>
<div class="empty-state">
<p class="text-muted small">No logs recorded yet.</p>
</div>
<?php else: ?>
<div class="list-group list-group-flush small">
<?php foreach ($recentLogs as $log): ?>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold"><?= h($log['action']) ?></span>
<span class="text-muted tiny"><?= h($log['created_at']) ?></span>
</div>
<div class="text-muted">by <?= h($log['user_name']) ?></div>
</div>
<?php endforeach; ?>
</div>
<a class="btn btn-link btn-sm mt-3 px-0" href="logs.php">View all logs</a>
<?php endif; ?>
</div>
</div>
<div class="card app-card mt-4">
<div class="card-body">
<h3 class="h6">Slash command coverage</h3>
<div class="command-grid">
<span>/play</span>
<span>/search</span>
<span>/queue</span>
<span>/skip</span>
<span>/pause</span>
<span>/resume</span>
<span>/stop</span>
<span>/loop</span>
<span>/shuffle</span>
<span>/volume</span>
<span>/nowplaying</span>
</div>
<p class="small text-muted mt-3">Commands map to the request queue and playback service.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="app-footer py-4">
<div class="container d-flex flex-column flex-md-row justify-content-between">
<span class="text-muted small">Runtime PHP <?= h($phpVersion) ?> • UTC <?= h($now) ?></span>
<span class="text-muted small">Next step: connect the bot worker to process queued requests.</span>
</div>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= (int)time() ?>"></script>
</body>
</html>

112
logs.php
View File

@ -1,112 +0,0 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pdo = db();
$logs = $pdo->query('SELECT * FROM bot_logs ORDER BY created_at DESC LIMIT 100')->fetchAll();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($projectName) ?> — Command Logs</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
<div class="container">
<a class="navbar-brand" href="index.php">EchoLift</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="requests.php">Queue</a>
<a class="nav-link active" href="logs.php">Logs</a>
<a class="nav-link" href="settings.php">Settings</a>
</div>
</div>
</div>
</nav>
<main class="app-shell">
<section class="container py-4 py-lg-5">
<div class="mb-4">
<p class="eyebrow">Audit Trail</p>
<h1 class="h3">Bot command logs</h1>
<p class="text-muted">Review every action taken by the bot or users in Discord server.</p>
</div>
<div class="card app-card">
<div class="card-body">
<?php if (!$logs): ?>
<div class="empty-state">
<p class="mb-1">No logs found.</p>
<p class="text-muted small mb-0">Logs will appear here once the bot starts processing commands.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Time</th>
<th>User</th>
<th>Action</th>
<th>Details</th>
<th>Server ID</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td class="text-muted small"><?= h($log['created_at']) ?></td>
<td class="fw-semibold"><?= h($log['user_name']) ?></td>
<td>
<span class="badge bg-light text-dark border"><?= h(strtoupper($log['action'])) ?></span>
</td>
<td><span class="small"><?= h($log['details'] ?? '-') ?></span></td>
<td class="text-muted small"><?= h($log['guild_id'] ?? 'N/A') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</section>
</main>
<footer class="app-footer py-4">
<div class="container d-flex flex-column flex-md-row justify-content-between">
<span class="text-muted small">Tracking bot interactions for security and stability.</span>
<span class="text-muted small">Showing last 100 entries.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= (int)time() ?>"></script>
</body>
</html>

View File

@ -1,172 +0,0 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$id = (int)($_GET['id'] ?? 0);
$statusOptions = ['queued', 'playing', 'paused', 'skipped', 'ended', 'failed'];
$statusUpdated = false;
$created = ($_GET['created'] ?? '') === '1';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_status') {
$newStatus = trim((string)($_POST['status'] ?? ''));
if (in_array($newStatus, $statusOptions, true) && $id > 0) {
$stmt = db()->prepare('UPDATE music_requests SET status = :status WHERE id = :id');
$stmt->execute([
':status' => $newStatus,
':id' => $id,
]);
header('Location: request.php?id=' . $id . '&updated=1');
exit;
}
}
$statusUpdated = ($_GET['updated'] ?? '') === '1';
$request = null;
if ($id > 0) {
$stmt = db()->prepare('SELECT * FROM music_requests WHERE id = :id');
$stmt->execute([':id' => $id]);
$request = $stmt->fetch();
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($projectName) ?> — Request</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
<div class="container">
<a class="navbar-brand" href="index.php">EchoLift</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="requests.php">Queue</a>
<a class="nav-link" href="logs.php">Logs</a>
<a class="nav-link" href="settings.php">Settings</a>
</div>
</div>
</div>
</nav>
<main class="app-shell">
<section class="container py-4 py-lg-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<div>
<p class="eyebrow">Request detail</p>
<h1 class="h3">Queue item overview</h1>
<p class="text-muted">Review the request metadata and update its status as the bot processes playback.</p>
</div>
<a class="btn btn-outline-secondary" href="requests.php">Back to queue</a>
</div>
<?php if (!$request): ?>
<div class="card app-card">
<div class="card-body">
<div class="empty-state">
<p class="mb-1">Request not found.</p>
<p class="text-muted small mb-0">Try selecting a request from the queue list.</p>
</div>
</div>
</div>
<?php else: ?>
<div class="row g-4">
<div class="col-lg-8">
<div class="card app-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h2 class="h4 mb-2"><?= h($request['query_text']) ?></h2>
<p class="text-muted mb-0">Requested by <?= h($request['requester_name']) ?> in <?= h($request['guild_name']) ?></p>
</div>
<span class="badge status-badge status-<?= h($request['status']) ?>"><?= h(ucfirst($request['status'])) ?></span>
</div>
<div class="detail-meta mt-4">
<div>
<p class="label">Source</p>
<p class="value"><?= h($request['source_type']) ?></p>
</div>
<div>
<p class="label">Voice channel</p>
<p class="value"><?= h($request['voice_channel'] ?: 'Not specified') ?></p>
</div>
<div>
<p class="label">Notes</p>
<p class="value"><?= h($request['notes'] ?: 'None') ?></p>
</div>
<div>
<p class="label">Created</p>
<p class="value"><?= h($request['created_at']) ?></p>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card app-card">
<div class="card-body">
<h3 class="h6">Update status</h3>
<form method="post" action="request.php?id=<?= (int)$request['id'] ?>" class="mt-3">
<input type="hidden" name="action" value="update_status">
<div class="mb-3">
<label class="form-label" for="status">Playback status</label>
<select class="form-select" name="status" id="status">
<?php foreach ($statusOptions as $option): ?>
<option value="<?= h($option) ?>" <?= $request['status'] === $option ? 'selected' : '' ?>><?= h(ucfirst($option)) ?></option>
<?php endforeach; ?>
</select>
</div>
<button class="btn btn-primary w-100" type="submit">Save status</button>
</form>
<div class="mt-4 small text-muted">Status changes sync to the queue list automatically.</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
</section>
</main>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<?php if ($created): ?>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body">Request created and queued for playback.</div>
</div>
<?php endif; ?>
<?php if ($statusUpdated): ?>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body">Status updated successfully.</div>
</div>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= (int)time() ?>"></script>
</body>
</html>

View File

@ -1,138 +0,0 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$statuses = ['all', 'queued', 'playing', 'paused', 'skipped', 'ended', 'failed'];
$filter = trim((string)($_GET['status'] ?? 'all'));
$filter = in_array($filter, $statuses, true) ? $filter : 'all';
if ($filter === 'all') {
$requests = db()->query('SELECT id, guild_name, requester_name, query_text, source_type, status, created_at FROM music_requests ORDER BY created_at DESC')->fetchAll();
} else {
$stmt = db()->prepare('SELECT id, guild_name, requester_name, query_text, source_type, status, created_at FROM music_requests WHERE status = :status ORDER BY created_at DESC');
$stmt->execute([':status' => $filter]);
$requests = $stmt->fetchAll();
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($projectName) ?> — Queue</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
<div class="container">
<a class="navbar-brand" href="index.php">EchoLift</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link active" href="requests.php">Queue</a>
<a class="nav-link" href="logs.php">Logs</a>
<a class="nav-link" href="settings.php">Settings</a>
</div>
</div>
</div>
</nav>
<main class="app-shell">
<section class="container py-4 py-lg-5">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end gap-3 mb-4">
<div>
<p class="eyebrow">Queue overview</p>
<h1 class="h3">Live request list</h1>
<p class="text-muted">Track every play request and update statuses as the bot processes them.</p>
</div>
<div class="d-flex gap-2 align-items-center">
<form method="get" action="requests.php">
<label class="form-label small text-muted" for="status">Filter status</label>
<select class="form-select" name="status" id="status" onchange="this.form.submit()">
<?php foreach ($statuses as $status): ?>
<option value="<?= h($status) ?>" <?= $filter === $status ? 'selected' : '' ?>><?= h(ucfirst($status)) ?></option>
<?php endforeach; ?>
</select>
</form>
<a class="btn btn-primary mt-4 mt-md-0" href="index.php#request-form">New request</a>
</div>
</div>
<div class="card app-card">
<div class="card-body">
<?php if (!$requests): ?>
<div class="empty-state">
<p class="mb-1">No requests found for this filter.</p>
<p class="text-muted small mb-0">Submit a song to start the queue.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Song</th>
<th>Server</th>
<th>Requester</th>
<th>Source</th>
<th>Status</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $request): ?>
<tr>
<td>
<a class="fw-semibold text-decoration-none" href="request.php?id=<?= (int)$request['id'] ?>">
<?= h($request['query_text']) ?>
</a>
</td>
<td><?= h($request['guild_name']) ?></td>
<td><?= h($request['requester_name']) ?></td>
<td><?= h($request['source_type']) ?></td>
<td><span class="badge status-badge status-<?= h($request['status']) ?>"><?= h(ucfirst($request['status'])) ?></span></td>
<td class="text-muted small"><?= h($request['created_at']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</section>
</main>
<footer class="app-footer py-4">
<div class="container d-flex flex-column flex-md-row justify-content-between">
<span class="text-muted small">Queue updates stay in sync with bot playback.</span>
<span class="text-muted small">Last refresh: <?= h(date('Y-m-d H:i:s')) ?> UTC</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= (int)time() ?>"></script>
</body>
</html>

View File

@ -1,283 +0,0 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$settings = get_settings();
$errors = [];
$saved = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_settings') {
$prefix = $_POST['prefix'] ?? '!';
$max_volume = (int)($_POST['max_volume'] ?? 100);
$auto_reconnect = isset($_POST['auto_reconnect']) ? 1 : 0;
$log_level = $_POST['log_level'] ?? 'info';
$discord_token = $_POST['discord_token'] ?? '';
$discord_app_id = $_POST['discord_app_id'] ?? '';
$discord_public_key = $_POST['discord_public_key'] ?? '';
$pdo = db();
$stmt = $pdo->prepare('UPDATE bot_settings SET
prefix = :prefix,
max_volume = :max_volume,
auto_reconnect = :auto_reconnect,
log_level = :log_level,
discord_token = :discord_token,
discord_app_id = :discord_app_id,
discord_public_key = :discord_public_key
WHERE id = :id');
$stmt->execute([
':prefix' => $prefix,
':max_volume' => $max_volume,
':auto_reconnect' => $auto_reconnect,
':log_level' => $log_level,
':discord_token' => $discord_token,
':discord_app_id' => $discord_app_id,
':discord_public_key' => $discord_public_key,
':id' => $settings['id'],
]);
add_log('Admin', 'Settings Update', 'Bot settings were updated.');
// If credentials changed, try to auto-sync slash commands
if ($discord_token !== $settings['discord_token'] || $discord_app_id !== $settings['discord_app_id']) {
require_once __DIR__ . '/bot/register_commands.php';
$syncResult = register_commands();
add_log('System', 'Auto-Sync Commands', $syncResult);
}
$settings = get_settings(); // Refresh
$saved = true;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'restart_bot') {
// Kill existing processes
shell_exec("pkill -f 'bot/worker.php'");
// Start new one
shell_exec("nohup php " . __DIR__ . "/bot/worker.php > " . __DIR__ . "/bot/bot.log 2>&1 &");
add_log('Admin', 'Worker Restart', 'The bot worker process was manually restarted from settings.');
$saved = true; // Trigger toast
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'sync_commands') {
require_once __DIR__ . '/bot/register_commands.php';
$syncResult = register_commands();
add_log('Admin', 'Manual Command Sync', $syncResult);
$saved = true;
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Discord Music Bot Control Center';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($projectName) ?> — Settings</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
<div class="container">
<a class="navbar-brand" href="index.php">EchoLift</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="requests.php">Queue</a>
<a class="nav-link" href="logs.php">Logs</a>
<a class="nav-link active" href="settings.php">Settings</a>
</div>
</div>
</div>
</nav>
<main class="app-shell">
<section class="container py-4 py-lg-5">
<div class="row g-4">
<div class="col-lg-7">
<p class="eyebrow">Administration</p>
<h1 class="h3">Bot policy settings</h1>
<p class="text-muted">Define default controls for who can use music commands and how playback is handled.</p>
<?php if ($errors): ?>
<div class="alert alert-warning mt-4" role="alert">
<?= h(implode(' ', $errors)) ?>
</div>
<?php endif; ?>
<div class="card app-card mt-4">
<div class="card-body">
<h2 class="h5 mb-3">Bot Credentials</h2>
<p class="small text-muted mb-4">Manage your Discord application keys. Keep these private.</p>
<form method="post" action="settings.php" class="row g-3">
<input type="hidden" name="action" value="save_settings">
<!-- Preserve existing values in hidden fields if they are not in this form,
but here we are updating the whole row, so better to include all fields in one form or handle partially.
Actually, I'll merge the forms into one for simplicity. -->
<div class="col-12">
<label class="form-label" for="discord_token">Bot Token</label>
<input type="password" class="form-control" id="discord_token" name="discord_token" value="<?= h((string)$settings['discord_token']) ?>" placeholder="MTAyNDU...">
<div class="form-text">Found in Discord Developer Portal > Bot > Token.</div>
</div>
<div class="col-md-6">
<label class="form-label" for="discord_app_id">Application ID</label>
<input class="form-control" id="discord_app_id" name="discord_app_id" value="<?= h((string)$settings['discord_app_id']) ?>" placeholder="1024...">
</div>
<div class="col-md-6">
<label class="form-label" for="discord_public_key">Public Key</label>
<input class="form-control" id="discord_public_key" name="discord_public_key" value="<?= h((string)$settings['discord_public_key']) ?>" placeholder="a1b2c3...">
</div>
<div class="col-12 mt-4 p-3 bg-light rounded border">
<h6 class="mb-2">Slash Commands Setup</h6>
<p class="small text-muted mb-2">To use slash commands like <code>/play</code>, copy this URL into your <strong>Discord Developer Portal > General Information > Interactions Endpoint URL</strong>:</p>
<div class="input-group input-group-sm">
<?php
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$interactionUrl = $protocol . $host . "/api/interactions.php";
?>
<input type="text" class="form-control bg-white" value="<?= h($interactionUrl) ?>" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="navigator.clipboard.writeText('<?= h($interactionUrl) ?>')">Copy</button>
</div>
<div class="form-text mt-2 text-primary">Note: Make sure your Public Key is saved above before Discord can verify this URL.</div>
</div>
<hr class="my-4">
<h2 class="h5 mb-3">Behavior Settings</h2>
<div class="col-md-6">
<label class="form-label" for="prefix">Command prefix</label>
<input class="form-control" id="prefix" name="prefix" value="<?= h($settings['prefix']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label" for="max_volume">Max volume (%)</label>
<input class="form-control" id="max_volume" name="max_volume" type="number" min="1" max="200" value="<?= h((string)$settings['max_volume']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label" for="log_level">Log level</label>
<select class="form-select" id="log_level" name="log_level">
<?php foreach (['debug', 'info', 'warn', 'error'] as $level): ?>
<option value="<?= h($level) ?>" <?= $settings['log_level'] === $level ? 'selected' : '' ?>><?= h(ucfirst($level)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="auto_reconnect" name="auto_reconnect" <?= $settings['auto_reconnect'] ? 'checked' : '' ?>>
<label class="form-check-label" for="auto_reconnect">Enable auto-reconnect when voice drops</label>
</div>
</div>
<div class="col-12 d-flex gap-2">
<button class="btn btn-primary" type="submit">Save all settings</button>
<a class="btn btn-outline-secondary" href="index.php">Back to dashboard</a>
</div>
</form>
<div class="mt-4 p-3 bg-light rounded border">
<h6 class="mb-2">Slash Commands Sync</h6>
<p class="small text-muted mb-2">If you've updated your Bot Token or App ID, you may need to manually refresh the commands on Discord.</p>
<form method="post" action="settings.php">
<input type="hidden" name="action" value="sync_commands">
<button type="submit" class="btn btn-sm btn-outline-primary">🔄 Sync Commands with Discord Now</button>
</form>
</div>
</div>
</div>
<div class="card app-card mt-4 border-danger-subtle">
<div class="card-body">
<h2 class="h5 text-danger mb-3">Process Control</h2>
<p class="small text-muted mb-4">Manage the background bot worker. Use this if the bot stops responding or after updating sensitive settings.</p>
<?php
$workerPid = shell_exec("pgrep -f 'bot/worker.php'");
$isWorkerRunning = !empty($workerPid);
?>
<div class="d-flex align-items-center justify-content-between">
<div>
<span class="badge <?= $isWorkerRunning ? 'bg-success' : 'bg-danger' ?> mb-1">
<?= $isWorkerRunning ? 'Running (PID: ' . trim($workerPid) . ')' : 'Stopped' ?>
</span>
<p class="mb-0 small text-muted">The worker process handles the queue and connections.</p>
</div>
<form method="post" action="settings.php">
<input type="hidden" name="action" value="restart_bot">
<button type="submit" class="btn btn-danger">
<?= $isWorkerRunning ? 'Restart Bot Worker' : 'Start Bot Worker' ?>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card app-card">
<div class="card-body">
<h2 class="h6">Policy preview</h2>
<div class="detail-meta mt-3">
<div>
<p class="label">Prefix</p>
<p class="value"><?= h($settings['prefix']) ?></p>
</div>
<div>
<p class="label">Max volume</p>
<p class="value"><?= h((string)$settings['max_volume']) ?>%</p>
</div>
<div>
<p class="label">Auto-reconnect</p>
<p class="value"><?= $settings['auto_reconnect'] ? 'Enabled' : 'Disabled' ?></p>
</div>
</div>
<p class="small text-muted mt-3">These defaults will be used by the bot runtime service.</p>
</div>
</div>
<div class="card app-card mt-4">
<div class="card-body">
<h2 class="h6">Stability checklist</h2>
<ul class="small text-muted mb-0">
<li>Auto-reconnect enabled for voice sessions.</li>
<li>Log level set to capture errors.</li>
<li>Volume limits keep playback stable.</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<?php if ($saved): ?>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body">Settings saved successfully.</div>
</div>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= (int)time() ?>"></script>
</body>
</html>