Compare commits

...

4 Commits

Author SHA1 Message Date
Flatlogic Bot
68832bd914 vvip 2026-02-18 09:24:47 +00:00
Flatlogic Bot
eb4f306c8d Autosave: 20260218-091142 2026-02-18 09:11:43 +00:00
Flatlogic Bot
3c6738376b savee 2026-02-18 09:01:53 +00:00
Flatlogic Bot
5ccb5169a6 vvv 2026-02-18 08:58:32 +00:00
12 changed files with 2355 additions and 123 deletions

7
.env Normal file
View File

@ -0,0 +1,7 @@
DISCORD_TOKEN=MTQ3Mjc2ODQ4NTQ0NzY5NjY1MA.GDeYeK.5LFkAzXfLpZjS2yn7976SQlU_Pr-zZmz4inptQ
CLIENT_ID=1472768485447696650
GUILD_ID=1428530728706117632
VOICE_CHANNEL_ID=1457687430189682781
SAHUR_HOUR=3
SAHUR_MINUTE=0
TEXT_CHANNEL_ID=1430856944004104326

117
assets/css/custom.css Normal file
View File

@ -0,0 +1,117 @@
:root {
color-scheme: light;
--bg: #f7f7f6;
--surface: #ffffff;
--text: #161616;
--muted: #6b6b6b;
--border: #e5e5e5;
--accent: #111111;
--accent-soft: #f0f0f0;
--radius-sm: 6px;
--radius-md: 10px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
}
body {
font-family: "Space Grotesk", "Segoe UI", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.navbar {
background: var(--surface) !important;
}
.hero {
background: var(--surface);
}
.section-head {
margin-bottom: var(--space-4);
}
.card,
.table {
border-color: var(--border) !important;
}
.card {
border-radius: var(--radius-md);
}
.btn {
border-radius: var(--radius-sm);
font-weight: 600;
}
.btn-dark {
background: var(--accent);
border-color: var(--accent);
}
.btn-outline-secondary {
border-color: var(--border);
color: var(--text);
}
.btn-outline-secondary:hover {
background: var(--accent-soft);
border-color: var(--border);
color: var(--text);
}
.dropzone {
border: 1px dashed var(--border);
border-radius: var(--radius-md);
padding: var(--space-5);
background: var(--surface);
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 140px;
transition: border-color 0.2s ease, background 0.2s ease;
}
.dropzone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.dropzone.is-dragover {
border-color: var(--text);
background: #fafafa;
}
.dropzone-placeholder {
display: flex;
flex-direction: column;
gap: var(--space-2);
text-align: center;
}
.table th {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.badge {
border-radius: 999px;
font-weight: 500;
}
.alert {
border-radius: var(--radius-md);
}
footer {
background: var(--surface);
}

40
assets/js/main.js Normal file
View File

@ -0,0 +1,40 @@
(() => {
const dropzones = document.querySelectorAll('[data-dropzone]');
dropzones.forEach((dropzone) => {
const input = dropzone.querySelector('input[type="file"]');
const fileLabel = dropzone.querySelector('[data-file-name]');
if (!input || !fileLabel) return;
const updateName = (file) => {
if (!file) {
fileLabel.textContent = 'Belum ada file dipilih';
return;
}
fileLabel.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
};
input.addEventListener('change', (event) => {
updateName(event.target.files[0]);
});
['dragenter', 'dragover'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
dropzone.classList.add('is-dragover');
});
});
['dragleave', 'drop'].forEach((eventName) => {
dropzone.addEventListener(eventName, () => {
dropzone.classList.remove('is-dragover');
});
});
dropzone.addEventListener('drop', (event) => {
event.preventDefault();
if (!event.dataTransfer.files.length) return;
input.files = event.dataTransfer.files;
updateName(event.dataTransfer.files[0]);
});
});
})();

26
bot.log Normal file
View File

@ -0,0 +1,26 @@
[dotenv@17.3.1] injecting env (7) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
Keep-Alive server running on port 8080
Bot logged in as XiaoMao#2565
Mendaftarkan ulang slash commands (setelah reset)...
(node:7936) DeprecationWarning: The ready event has been renamed to clientReady to distinguish it from the gateway READY event and will only emit under that name in v15. Please use clientReady instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
Berhasil mendaftarkan command: /join, /test_sahur
/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:746
throw new Error(
^
Error: Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed.
- Use the generateDependencyReport() function for more information.
at new DAVESession (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:746:13)
at Networking.createDaveSession (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1467:21)
at Networking.onWsPacket (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1602:20)
at VoiceWebSocket.emit (node:events:530:35)
at VoiceWebSocket.onMessage (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1239:10)
at VoiceWebSocket.ws.onmessage (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1182:39)
at callListener (/home/ubuntu/executor/workspace/node_modules/ws/lib/event-target.js:290:14)
at WebSocket.onMessage (/home/ubuntu/executor/workspace/node_modules/ws/lib/event-target.js:209:9)
at WebSocket.emit (node:events:518:28)
at Receiver.receiverOnMessage (/home/ubuntu/executor/workspace/node_modules/ws/lib/websocket.js:1225:20)
Node.js v22.18.0

102
event.php Normal file
View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('Asia/Jakarta');
require_once __DIR__ . '/db/config.php';
$eventId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$pdo = db();
$event = null;
if ($eventId) {
$stmt = $pdo->prepare('SELECT id, event_type, message, meta, created_at FROM sahur_events WHERE id = :id');
$stmt->execute([':id' => $eventId]);
$event = $stmt->fetch();
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Detail Aktivitas Sahur</title>
<?php if ($projectDescription): ?>
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<meta name="robots" content="noindex, nofollow" />
<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=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/custom.css?v=<?= htmlspecialchars((string)time()) ?>" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-white border-bottom">
<div class="container">
<a class="navbar-brand fw-semibold" href="/">Sahur Bot Control</a>
</div>
</nav>
<main class="container py-5">
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<p class="text-uppercase text-muted small mb-1">Detail Aktivitas</p>
<h1 class="h4 fw-semibold mb-0">Log Sahur</h1>
</div>
<a class="btn btn-outline-secondary" href="/">Kembali</a>
</div>
<?php if (!$event): ?>
<div class="alert alert-warning" role="alert">
Data aktivitas tidak ditemukan. Pilih event dari daftar log.
</div>
<?php else: ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<p class="text-muted small mb-1">Tipe</p>
<p class="fw-semibold mb-0 text-uppercase"><?= htmlspecialchars($event['event_type']) ?></p>
</div>
<div class="col-md-4">
<p class="text-muted small mb-1">Waktu</p>
<p class="fw-semibold mb-0"><?= htmlspecialchars($event['created_at']) ?> WIB</p>
</div>
<div class="col-md-4">
<p class="text-muted small mb-1">ID Event</p>
<p class="fw-semibold mb-0">#<?= htmlspecialchars((string)$event['id']) ?></p>
</div>
<div class="col-12">
<p class="text-muted small mb-1">Pesan</p>
<p class="mb-0"><?= htmlspecialchars($event['message']) ?></p>
</div>
<div class="col-12">
<p class="text-muted small mb-1">Meta</p>
<pre class="bg-light border rounded p-3 mb-0 small"><?= htmlspecialchars($event['meta'] ?: 'Tidak ada data tambahan.') ?></pre>
</div>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="border-top py-4">
<div class="container">
<span class="text-muted small">Sahur Bot Dashboard (MVP)</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

184
index.js Normal file
View File

@ -0,0 +1,184 @@
require('dotenv').config();
const { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } = require('discord.js');
const {
joinVoiceChannel,
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
NoSubscriberBehavior,
VoiceConnectionStatus,
enterState
} = require('@discordjs/voice');
const express = require('express');
const fs = require('fs');
const path = require('path');
// --- Configuration ---
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
const CLIENT_ID = process.env.CLIENT_ID;
const GUILD_ID = process.env.GUILD_ID;
// Keep-Alive Server
const app = express();
const port = 8080;
app.get('/', (req, res) => res.send('Bot Sahur is Alive! 🌙'));
app.listen(port, () => console.log(`Keep-Alive server running on port ${port}`));
// Discord Client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates
]
});
let player = null;
let connection = null;
let isLooping = false;
// Function to play local sahur.mp3
function playAudio() {
try {
if (!connection) {
console.error('[Audio] Connection tidak ditemukan!');
return false;
}
const filePath = path.join(__dirname, 'sahur.mp3');
console.log(`[Audio] Memutar file dari: ${filePath}`);
if (!fs.existsSync(filePath)) {
console.error(`[Audio] File TIDAK ditemukan di: ${filePath}`);
return false;
}
if (!player) {
console.log('[Audio] Membuat player baru...');
player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
player.on(AudioPlayerStatus.Idle, () => {
if (isLooping && connection) {
console.log('[Audio] Looping: Memutar ulang sahur.mp3');
playAudio();
}
});
player.on('error', error => {
console.error('[Audio] Player Error:', error.message);
console.error(error);
});
}
const resource = createAudioResource(filePath);
player.play(resource);
connection.subscribe(player);
console.log('[Audio] Berhasil memutar resource.');
return true;
} catch (error) {
console.error('[Audio] Exception di playAudio:', error);
return false;
}
}
// Slash Commands Definition
const commands = [
new SlashCommandBuilder()
.setName('join')
.setDescription('Membuat bot masuk ke Voice Channel kamu'),
new SlashCommandBuilder()
.setName('test_sahur')
.setDescription('Memutar sahur.mp3 secara berulang (loop)'),
].map(command => command.toJSON());
const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);
async function registerCommands() {
try {
console.log('Mendaftarkan ulang slash commands (setelah reset)...');
await rest.put(
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
{ body: commands }
);
console.log('Berhasil mendaftarkan command: /join, /test_sahur');
} catch (error) {
console.error('Gagal mendaftarkan command:', error);
}
}
client.once('ready', () => {
console.log(`Bot logged in as ${client.user.tag}`);
registerCommands();
// Simple Keep-Alive log every 15 minutes
setInterval(() => {
console.log(`[Keep-Alive] Bot active at ${new Date().toISOString()}`);
}, 15 * 60 * 1000);
});
client.on('interactionCreate', async interaction => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
try {
if (commandName === 'join') {
const member = interaction.member;
const voiceChannel = member.voice.channel;
if (!voiceChannel) {
return interaction.reply({ content: 'Kamu harus berada di Voice Channel!', ephemeral: true });
}
connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: interaction.guildId,
adapterCreator: interaction.guild.voiceAdapterCreator,
});
connection.on(VoiceConnectionStatus.Disconnected, async (oldState, newState) => {
try {
await Promise.race([
enterState(connection, VoiceConnectionStatus.Signalling, 5_000),
enterState(connection, VoiceConnectionStatus.Connecting, 5_000),
]);
} catch (error) {
console.error('[Voice] Gagal menyambung kembali:', error);
if (connection) connection.destroy();
connection = null;
}
});
await interaction.reply({ content: `Bot berhasil join ke ${voiceChannel.name}! ✅` });
}
if (commandName === 'test_sahur') {
if (!connection) {
return interaction.reply({ content: 'Bot harus join ke Voice Channel dulu! Gunakan /join', ephemeral: true });
}
isLooping = true;
const success = playAudio();
if (success) {
await interaction.reply({ content: 'Memutar sahur.mp3 dalam mode loop... 🔁' });
} else {
await interaction.reply({ content: 'Gagal memutar audio. Cek Logs untuk detailnya. ❌', ephemeral: true });
}
}
} catch (error) {
console.error(`[Interaction Error] Command: ${commandName}`, error);
const errorMsg = 'Terjadi kesalahan saat menjalankan command ini. Silakan coba lagi.';
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: errorMsg, ephemeral: true });
} else {
await interaction.reply({ content: errorMsg, ephemeral: true });
}
}
});
client.login(DISCORD_TOKEN);

457
index.php
View File

@ -2,149 +2,360 @@
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
@date_default_timezone_set('Asia/Jakarta');
session_start();
require_once __DIR__ . '/db/config.php';
$pdo = db();
$pdo->exec(
'CREATE TABLE IF NOT EXISTS sahur_settings ('
. 'id INT PRIMARY KEY, '
. 'sahur_hour TINYINT NOT NULL, '
. 'sahur_minute TINYINT NOT NULL, '
. 'updated_at DATETIME NOT NULL'
. ')'
);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS sahur_events ('
. 'id INT AUTO_INCREMENT PRIMARY KEY, '
. 'event_type VARCHAR(32) NOT NULL, '
. 'message VARCHAR(255) NOT NULL, '
. 'meta TEXT NULL, '
. 'created_at DATETIME NOT NULL'
. ')'
);
$settings = $pdo->query('SELECT id, sahur_hour, sahur_minute, updated_at FROM sahur_settings WHERE id = 1')->fetch();
if (!$settings) {
$stmt = $pdo->prepare('INSERT INTO sahur_settings (id, sahur_hour, sahur_minute, updated_at) VALUES (1, :hour, :minute, NOW())');
$stmt->execute([':hour' => 3, ':minute' => 0]);
$settings = ['id' => 1, 'sahur_hour' => 3, 'sahur_minute' => 0, 'updated_at' => date('Y-m-d H:i:s')];
}
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
function redirect_with_flash(string $type, string $message): void {
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
header('Location: /');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'upload') {
if (!isset($_FILES['sahur_file']) || $_FILES['sahur_file']['error'] !== UPLOAD_ERR_OK) {
redirect_with_flash('danger', 'Upload gagal. Pilih file MP3 yang valid.');
}
$file = $_FILES['sahur_file'];
if ($file['size'] > 15000000) {
redirect_with_flash('danger', 'Ukuran file terlalu besar. Maksimum 15MB.');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo ? finfo_file($finfo, $file['tmp_name']) : '';
if ($finfo) {
finfo_close($finfo);
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($mime !== 'audio/mpeg' || $ext !== 'mp3') {
redirect_with_flash('danger', 'Format file harus MP3 (audio/mpeg).');
}
$uploadDir = __DIR__ . '/uploads';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$targetPath = $uploadDir . '/sahur.mp3';
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
redirect_with_flash('danger', 'Upload gagal saat menyimpan file.');
}
$meta = json_encode([
'original_name' => $file['name'],
'size' => $file['size'],
]);
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
$stmt->execute([
':type' => 'upload',
':message' => 'File sahur.mp3 diperbarui',
':meta' => $meta,
]);
redirect_with_flash('success', 'File sahur.mp3 berhasil diunggah dan diganti.');
}
if ($action === 'set_schedule') {
$hour = filter_input(INPUT_POST, 'sahur_hour', FILTER_VALIDATE_INT);
$minute = filter_input(INPUT_POST, 'sahur_minute', FILTER_VALIDATE_INT);
if ($hour === false || $minute === false || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
redirect_with_flash('danger', 'Jam sahur tidak valid. Masukkan 0-23 untuk jam dan 0-59 untuk menit.');
}
$stmt = $pdo->prepare('UPDATE sahur_settings SET sahur_hour = :hour, sahur_minute = :minute, updated_at = NOW() WHERE id = 1');
$stmt->execute([':hour' => $hour, ':minute' => $minute]);
$meta = json_encode(['hour' => $hour, 'minute' => $minute]);
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
$stmt->execute([
':type' => 'schedule',
':message' => 'Jadwal sahur diperbarui',
':meta' => $meta,
]);
redirect_with_flash('success', 'Jadwal sahur berhasil diperbarui.');
}
if ($action === 'test_now') {
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
$stmt->execute([
':type' => 'test',
':message' => 'Tes manual dipicu dari dashboard',
':meta' => null,
]);
redirect_with_flash('success', 'Tes sahur dicatat. Integrasi bot akan memutar audio saat tersedia.');
}
}
$audioPath = __DIR__ . '/uploads/sahur.mp3';
$fileExists = is_file($audioPath);
$fileSize = $fileExists ? filesize($audioPath) : null;
$fileUpdated = $fileExists ? date('Y-m-d H:i', filemtime($audioPath)) : null;
$now = new DateTime('now');
$nextRun = (clone $now)->setTime((int)$settings['sahur_hour'], (int)$settings['sahur_minute'], 0);
if ($nextRun <= $now) {
$nextRun->modify('+1 day');
}
$events = $pdo->query('SELECT id, event_type, message, created_at FROM sahur_events ORDER BY id DESC LIMIT 8')->fetchAll();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sahur Bot Control Center</title>
<?php if ($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): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<meta name="robots" content="noindex, nofollow" />
<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>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/custom.css?v=<?= htmlspecialchars((string)time()) ?>" rel="stylesheet">
</head>
<body>
<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>
<nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top">
<div class="container">
<a class="navbar-brand fw-semibold" href="/">Sahur Bot Control</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navMain">
<div class="navbar-nav ms-auto">
<a class="nav-link" href="#status">Status</a>
<a class="nav-link" href="#upload">Upload</a>
<a class="nav-link" href="#schedule">Jadwal</a>
<a class="nav-link" href="#log">Log</a>
</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>
</nav>
<header class="hero border-bottom">
<div class="container py-5">
<div class="row align-items-center g-4">
<div class="col-lg-7">
<p class="text-uppercase text-muted small mb-2">Dashboard Sahur</p>
<h1 class="display-6 fw-semibold mb-3">Kendalikan jadwal sahur dan audio bot dalam satu tempat.</h1>
<p class="text-muted mb-4">Upload MP3, ubah jam sahur, dan catat tes manual. Status file dan jadwal berikutnya selalu terlihat jelas.</p>
<div class="d-flex gap-3">
<a class="btn btn-dark btn-lg" href="#upload">Upload MP3</a>
<a class="btn btn-outline-secondary btn-lg" href="#schedule">Ubah Jadwal</a>
</div>
</div>
<div class="col-lg-5">
<div class="card shadow-sm border-0">
<div class="card-body">
<p class="text-muted small mb-1">Next run</p>
<h2 class="h4 fw-semibold mb-2"><?= htmlspecialchars($nextRun->format('d M Y, H:i')) ?> WIB</h2>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-light text-dark border"><?= htmlspecialchars(sprintf('%02d:%02d', $settings['sahur_hour'], $settings['sahur_minute'])) ?> (jadwal aktif)</span>
<span class="badge bg-light text-dark border"><?= $fileExists ? 'File MP3 tersedia' : 'File MP3 belum ada' ?></span>
</div>
<p class="text-muted small mt-3 mb-0">Pastikan bot Discord kamu membaca jadwal yang sama.</p>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="container py-5">
<?php if ($flash): ?>
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
<?= htmlspecialchars($flash['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<section id="status" class="mb-5">
<div class="section-head">
<h2 class="h5 fw-semibold mb-1">Status Saat Ini</h2>
<p class="text-muted small">Pantau kesiapan audio dan jadwal sahur berikutnya.</p>
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<p class="text-muted small mb-1">Audio sahur</p>
<h3 class="h6 fw-semibold mb-2"><?= $fileExists ? 'Tersedia' : 'Belum tersedia' ?></h3>
<p class="text-muted small mb-0"><?= $fileExists ? 'Ukuran: ' . number_format($fileSize / 1024, 1) . ' KB' : 'Upload file sahur.mp3 agar bot dapat memutar.' ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<p class="text-muted small mb-1">Update terakhir</p>
<h3 class="h6 fw-semibold mb-2"><?= $fileUpdated ? htmlspecialchars($fileUpdated . ' WIB') : '—' ?></h3>
<p class="text-muted small mb-0">Penggantian file terakhir.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<p class="text-muted small mb-1">Next run</p>
<h3 class="h6 fw-semibold mb-2"><?= htmlspecialchars($nextRun->format('d M Y, H:i')) ?> WIB</h3>
<p class="text-muted small mb-0">Jadwal sahur berdasarkan setting saat ini.</p>
</div>
</div>
</div>
</div>
<?php if (!$fileExists): ?>
<div class="alert alert-warning mt-3 mb-0" role="alert">
File sahur.mp3 belum ada. Bot Discord harus menampilkan peringatan di channel teks saat file belum tersedia.
</div>
<?php endif; ?>
</section>
<section id="upload" class="mb-5">
<div class="section-head">
<h2 class="h5 fw-semibold mb-1">Upload MP3 Sahur</h2>
<p class="text-muted small">Drag &amp; drop atau klik untuk memilih file. File akan ditimpa menjadi sahur.mp3.</p>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form class="row g-3" action="/" method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload" />
<div class="col-12">
<div class="dropzone" data-dropzone>
<input class="form-control" type="file" name="sahur_file" accept=".mp3,audio/mpeg" required>
<div class="dropzone-placeholder">
<span class="fw-semibold">Tarik file ke sini</span>
<span class="text-muted small">atau klik untuk memilih</span>
<span class="text-muted small" data-file-name>Belum ada file dipilih</span>
</div>
</div>
</div>
<div class="col-12 d-flex justify-content-end">
<button type="submit" class="btn btn-dark">Upload &amp; Ganti File</button>
</div>
</form>
</div>
</div>
</section>
<section id="schedule" class="mb-5">
<div class="section-head">
<h2 class="h5 fw-semibold mb-1">Jadwal Sahur</h2>
<p class="text-muted small">Atur jam sahur harian yang akan digunakan bot.</p>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form class="row g-3 align-items-end" action="/" method="post">
<input type="hidden" name="action" value="set_schedule" />
<div class="col-md-4">
<label class="form-label" for="sahur_hour">Jam</label>
<input class="form-control" type="number" id="sahur_hour" name="sahur_hour" min="0" max="23" value="<?= htmlspecialchars((string)$settings['sahur_hour']) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label" for="sahur_minute">Menit</label>
<input class="form-control" type="number" id="sahur_minute" name="sahur_minute" min="0" max="59" value="<?= htmlspecialchars((string)$settings['sahur_minute']) ?>" required>
</div>
<div class="col-md-4 d-flex gap-2">
<button type="submit" class="btn btn-dark flex-fill">Simpan Jadwal</button>
<button type="submit" class="btn btn-outline-secondary flex-fill" name="action" value="test_now">Tes Sekarang</button>
</div>
</form>
</div>
</div>
</section>
<section id="log" class="mb-5">
<div class="section-head">
<h2 class="h5 fw-semibold mb-1">Log Aktivitas</h2>
<p class="text-muted small">Riwayat upload, perubahan jadwal, dan tes manual.</p>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<?php if (!$events): ?>
<p class="text-muted mb-0">Belum ada aktivitas. Mulai dengan upload file sahur pertama.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th scope="col">Waktu</th>
<th scope="col">Tipe</th>
<th scope="col">Pesan</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<?php foreach ($events as $event): ?>
<tr>
<td class="text-muted small"><?= htmlspecialchars($event['created_at']) ?></td>
<td><span class="badge bg-light text-dark border text-uppercase"><?= htmlspecialchars($event['event_type']) ?></span></td>
<td><?= htmlspecialchars($event['message']) ?></td>
<td class="text-end"><a class="link-dark" href="/event.php?id=<?= htmlspecialchars((string)$event['id']) ?>">Detail</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</section>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
<footer class="border-top py-4">
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
<span class="text-muted small">Sahur Bot Dashboard (MVP)</span>
<span class="text-muted small">Zona waktu: WIB (Asia/Jakarta)</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/main.js?v=<?= htmlspecialchars((string)time()) ?>" defer></script>
</body>
</html>

1522
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "workspace",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.19.0",
"discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"ffmpeg-static": "^5.3.0",
"libsodium-wrappers": "^0.8.2",
"opusscript": "^0.0.8"
}
}

BIN
sahur.mp3 Normal file

Binary file not shown.

1
uploads/README.txt Normal file
View File

@ -0,0 +1 @@
Place sahur.mp3 in this folder.

BIN
uploads/sahur.mp3 Normal file

Binary file not shown.