Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27278869f |
164
api/interactions.php
Normal file
164
api/interactions.php
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
223
assets/css/custom.css
Normal file
223
assets/css/custom.css
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
: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));
|
||||||
|
}
|
||||||
|
}
|
||||||
6
assets/js/main.js
Normal file
6
assets/js/main.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.toast').forEach((toastEl) => {
|
||||||
|
const toast = new bootstrap.Toast(toastEl, { delay: 4000 });
|
||||||
|
toast.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
assets/pasted-20260218-053424-209d7a8d.jpg
Normal file
BIN
assets/pasted-20260218-053424-209d7a8d.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 KiB |
3
bot/bot.log
Normal file
3
bot/bot.log
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[*] Discord Bot Worker started...
|
||||||
|
[+] Processing request #1: 'Bawa dia kembali' for server 'LAST XPERIENCE STUDIO'
|
||||||
|
[#] Request #1 completed.
|
||||||
64
bot/register_commands.php
Normal file
64
bot/register_commands.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
75
bot/worker.php
Normal file
75
bot/worker.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?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
|
||||||
|
}
|
||||||
|
}
|
||||||
120
includes/app.php
Normal file
120
includes/app.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?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
410
index.php
@ -4,147 +4,309 @@ declare(strict_types=1);
|
|||||||
@error_reporting(E_ALL);
|
@error_reporting(E_ALL);
|
||||||
@date_default_timezone_set('UTC');
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/app.php';
|
||||||
|
|
||||||
|
ensure_tables();
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
$phpVersion = PHP_VERSION;
|
||||||
$now = date('Y-m-d H:i:s');
|
$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>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>New Style</title>
|
<title><?= h($projectName) ?></title>
|
||||||
<?php
|
|
||||||
// Read project preview data from environment
|
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
||||||
?>
|
|
||||||
<?php if ($projectDescription): ?>
|
<?php if ($projectDescription): ?>
|
||||||
<!-- Meta description -->
|
<meta name="description" content="<?= h($projectDescription) ?>" />
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
<meta property="og:description" content="<?= h($projectDescription) ?>" />
|
||||||
<!-- Open Graph meta tags -->
|
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<!-- Twitter meta tags -->
|
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($projectImageUrl): ?>
|
<?php if ($projectImageUrl): ?>
|
||||||
<!-- Open Graph image -->
|
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
|
||||||
<!-- Twitter image -->
|
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<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="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= (int)time() ?>">
|
||||||
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<nav class="navbar navbar-expand-lg sticky-top app-navbar">
|
||||||
<div class="card">
|
<div class="container">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<a class="navbar-brand" href="index.php">EchoLift</a>
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<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="sr-only">Loading…</span>
|
<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>
|
||||||
</div>
|
</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>
|
</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>
|
</main>
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<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>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
112
logs.php
Normal file
112
logs.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?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>
|
||||||
172
request.php
Normal file
172
request.php
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<?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>
|
||||||
138
requests.php
Normal file
138
requests.php
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<?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>
|
||||||
283
settings.php
Normal file
283
settings.php
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<?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>
|
||||||
Loading…
x
Reference in New Issue
Block a user