Jadwal 2 mode, reset di di box, test button ampli on, dan bel manual
This commit is contained in:
496
data/index.html
496
data/index.html
@@ -1,65 +1,245 @@
|
||||
<!-- data/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Bel Sekolah</title>
|
||||
<style>
|
||||
/* Inline Tailwind CSS for offline compatibility */
|
||||
.bg-gradient-to-b { background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); }
|
||||
.from-slate-50 { --tw-gradient-from: #f8fafc; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(248, 250, 252, 0)); }
|
||||
.to-white { --tw-gradient-to: #ffffff; }
|
||||
.min-h-screen { min-height: 100vh; }
|
||||
.font-sans { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
||||
.text-slate-800 { --tw-text-opacity: 1; color: rgb(30 41 59 / var(--tw-text-opacity)); }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.text-center { text-align: center; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.text-5xl { font-size: 3rem; line-height: 1; }
|
||||
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.text-blue-600 { --tw-text-opacity: 1; color: rgb(37 99 235 / var(--tw-text-opacity)); }
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-slate-600 { --tw-text-opacity: 1; color: rgb(71 85 105 / var(--tw-text-opacity)); }
|
||||
.mt-1 { margin-top: 0.25rem; }
|
||||
.bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); }
|
||||
.shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-width 0px), var(--tw-ring-shadow), var(--tw-shadow); }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.text-5xl { font-size: 3rem; line-height: 1; }
|
||||
.font-extrabold { font-weight: 800; }
|
||||
.tracking-wider { letter-spacing: 0.05em; }
|
||||
.text-slate-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.my-3 { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
||||
.w-3 { width: 0.75rem; }
|
||||
.h-3 { height: 0.75rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
.bg-gray-300 { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); }
|
||||
.text-slate-700 { --tw-text-opacity: 1; color: rgb(51 65 85 / var(--tw-text-opacity)); }
|
||||
.grid { display: grid; }
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
.max-w-md { max-width: 28rem; }
|
||||
.bg-slate-50 { --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.rounded { border-radius: 0.25rem; }
|
||||
.h-40 { height: 10rem; }
|
||||
.overflow-auto { overflow: auto; }
|
||||
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.text-slate-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
.bg-gradient-to-b {
|
||||
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
|
||||
}
|
||||
|
||||
.from-slate-50 {
|
||||
--tw-gradient-from: #f8fafc;
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(248, 250, 252, 0));
|
||||
}
|
||||
|
||||
.to-white {
|
||||
--tw-gradient-to: #ffffff;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
}
|
||||
|
||||
.text-slate-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 41 59 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text-slate-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(71 85 105 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-width 0px), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.font-extrabold {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tracking-wider {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.text-slate-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.bg-gray-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.text-slate-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.bg-slate-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(248 250 252 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.h-40 {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.text-slate-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
@@ -71,19 +251,46 @@
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.btn.small { padding: 0.25rem 0.5rem; font-size: 0.9rem; }
|
||||
.btn.btn-blue { background: #2563EB; }
|
||||
.btn.btn-green { background: #059669; }
|
||||
.btn.btn-red { background: #DC2626; }
|
||||
.btn.btn-gray { background: #6B7280; }
|
||||
.btn.btn-purple { background: #7C3AED; }
|
||||
.btn:hover { filter: brightness(0.95); }
|
||||
|
||||
.btn.small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn.btn-blue {
|
||||
background: #2563EB;
|
||||
}
|
||||
|
||||
.btn.btn-green {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn.btn-red {
|
||||
background: #DC2626;
|
||||
}
|
||||
|
||||
.btn.btn-gray {
|
||||
background: #6B7280;
|
||||
}
|
||||
|
||||
.btn.btn-purple {
|
||||
background: #7C3AED;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.btn { padding: 0.45rem 0.7rem; font-size: 0.9rem; }
|
||||
.btn {
|
||||
padding: 0.45rem 0.7rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-b from-slate-50 to-white min-h-screen font-sans text-slate-800">
|
||||
<div class="max-w-2xl mx-auto p-6">
|
||||
<header class="text-center mb-6">
|
||||
@@ -120,93 +327,96 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// WebSocket connection (uses same host)
|
||||
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(protocol + location.host + '/ws');
|
||||
<script>
|
||||
// WebSocket connection (uses same host)
|
||||
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(protocol + location.host + '/ws');
|
||||
|
||||
const hariEl = document.getElementById('hari');
|
||||
const clockEl = document.getElementById('clock');
|
||||
const tanggalEl = document.getElementById('tanggal');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const schoolEl = document.getElementById('schoolName');
|
||||
const logEl = document.getElementById('log');
|
||||
const hariEl = document.getElementById('hari');
|
||||
const clockEl = document.getElementById('clock');
|
||||
const tanggalEl = document.getElementById('tanggal');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const schoolEl = document.getElementById('schoolName');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function appendLog(s) {
|
||||
const t = document.createElement('div');
|
||||
t.textContent = (new Date()).toLocaleTimeString() + " — " + s;
|
||||
logEl.prepend(t);
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
appendLog("🔌 WebSocket tersambung.");
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
appendLog("⚠️ WebSocket terputus.");
|
||||
statusDot.style.background = 'gray';
|
||||
statusText.textContent = 'Terputus';
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === 'time') {
|
||||
const h = String(msg.jam).padStart(2,'0');
|
||||
const m = String(msg.menit).padStart(2,'0');
|
||||
const s = String(msg.detik).padStart(2,'0');
|
||||
clockEl.textContent = `${h}.${m}.${s}`;
|
||||
const weekday = ["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"];
|
||||
hariEl.textContent = weekday[msg.weekday || new Date(msg.epoch*1000).getDay()];
|
||||
const monthNames = ["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"];
|
||||
tanggalEl.textContent = `${msg.hari} ${monthNames[(msg.bulan||(new Date(msg.epoch*1000)).getMonth())-1]} ${msg.tahun}`;
|
||||
} else if (msg.type === 'status') {
|
||||
if (msg.playing) {
|
||||
statusDot.style.background = 'green';
|
||||
statusText.textContent = `Bel berbunyi (Track ${msg.track})`;
|
||||
} else {
|
||||
statusDot.style.background = 'gray';
|
||||
statusText.textContent = 'Bel siaga';
|
||||
}
|
||||
if (msg.schoolName) schoolEl.textContent = msg.schoolName;
|
||||
|
||||
// Update clients info
|
||||
const clientsInfoEl = document.getElementById('clientsInfo');
|
||||
if (msg.clientList && msg.clientList.length > 0) {
|
||||
let info = '';
|
||||
msg.clientList.forEach((client, index) => {
|
||||
if (index > 0) info += ', ';
|
||||
info += `${client.mac}: ${client.percentage}%`;
|
||||
});
|
||||
clientsInfoEl.textContent = info;
|
||||
} else {
|
||||
clientsInfoEl.textContent = '';
|
||||
}
|
||||
} else if (msg.type === 'log') {
|
||||
appendLog(msg.msg);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("ws onmessage parse err", e);
|
||||
function appendLog(s) {
|
||||
const t = document.createElement('div');
|
||||
t.textContent = (new Date()).toLocaleTimeString() + " — " + s;
|
||||
logEl.prepend(t);
|
||||
}
|
||||
};
|
||||
|
||||
// Buttons
|
||||
document.getElementById('btnTest').addEventListener('click', async () => {
|
||||
// call protected API -> will prompt basic auth
|
||||
// test uses track 1000 (same as button click)
|
||||
try {
|
||||
await fetch('/api/play?track=1000');
|
||||
} catch(e) { console.error(e); }
|
||||
});
|
||||
ws.onopen = () => {
|
||||
appendLog("🔌 WebSocket tersambung.");
|
||||
};
|
||||
|
||||
document.getElementById('btnStop').addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/stop');
|
||||
} catch(e) { console.error(e); }
|
||||
});
|
||||
ws.onclose = () => {
|
||||
appendLog("⚠️ WebSocket terputus.");
|
||||
statusDot.style.background = 'gray';
|
||||
statusText.textContent = 'Terputus';
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === 'time') {
|
||||
const h = String(msg.jam).padStart(2, '0');
|
||||
const m = String(msg.menit).padStart(2, '0');
|
||||
const s = String(msg.detik).padStart(2, '0');
|
||||
clockEl.textContent = `${h}.${m}.${s}`;
|
||||
const weekday = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
|
||||
hariEl.textContent = weekday[msg.weekday || new Date(msg.epoch * 1000).getDay()];
|
||||
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
|
||||
tanggalEl.textContent = `${msg.hari} ${monthNames[(msg.bulan || (new Date(msg.epoch * 1000)).getMonth()) - 1]} ${msg.tahun}`;
|
||||
} else if (msg.type === 'status') {
|
||||
if (msg.playing) {
|
||||
statusDot.style.background = 'green';
|
||||
statusText.textContent = `Bel berbunyi (Track ${msg.track})`;
|
||||
} else {
|
||||
statusDot.style.background = 'gray';
|
||||
statusText.textContent = 'Bel siaga';
|
||||
}
|
||||
if (msg.schoolName) schoolEl.textContent = msg.schoolName;
|
||||
|
||||
// Update clients info
|
||||
const clientsInfoEl = document.getElementById('clientsInfo');
|
||||
if (msg.clientList && msg.clientList.length > 0) {
|
||||
let info = '';
|
||||
msg.clientList.forEach((client, index) => {
|
||||
if (index > 0) info += ', ';
|
||||
info += `${client.mac}: ${client.percentage}%`;
|
||||
});
|
||||
clientsInfoEl.textContent = info;
|
||||
} else {
|
||||
clientsInfoEl.textContent = '';
|
||||
}
|
||||
} else if (msg.type === 'log') {
|
||||
appendLog(msg.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("ws onmessage parse err", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Buttons
|
||||
document.getElementById('btnTest').addEventListener('click', async () => {
|
||||
try {
|
||||
// Fetch testTrack from config first
|
||||
const configRes = await fetch('/api/config');
|
||||
const config = await configRes.json();
|
||||
const track = config.testTrack || 1000;
|
||||
await fetch(`/api/play?track=${track}`);
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
document.getElementById('btnStop').addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/stop');
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -515,6 +515,35 @@
|
||||
.th-desc {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Day tabs */
|
||||
.day-tab {
|
||||
background-color: transparent;
|
||||
color: #64748b;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.day-tab:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.day-tab.active {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.day-tab[data-day="0"],
|
||||
.day-tab[data-day="6"] {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.day-tab[data-day="0"].active,
|
||||
.day-tab[data-day="6"].active {
|
||||
background-color: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -584,7 +613,7 @@
|
||||
|
||||
<!-- Tab: Jadwal -->
|
||||
<div id="tab-jadwal" class="tab-content block">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold text-slate-800">Jadwal</h2>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="tambah()" class="btn btn-green small">➕ Tambah</button>
|
||||
@@ -592,6 +621,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Day Tabs (only shown in 7-day mode) -->
|
||||
<div id="dayTabsContainer" class="hidden mb-4">
|
||||
<div class="flex flex-wrap gap-1 bg-slate-100 p-1 rounded-lg">
|
||||
<button onclick="switchDayTab(0)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="0">Minggu</button>
|
||||
<button onclick="switchDayTab(1)" class="day-tab px-3 py-2 rounded text-sm font-medium active"
|
||||
data-day="1">Senin</button>
|
||||
<button onclick="switchDayTab(2)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="2">Selasa</button>
|
||||
<button onclick="switchDayTab(3)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="3">Rabu</button>
|
||||
<button onclick="switchDayTab(4)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="4">Kamis</button>
|
||||
<button onclick="switchDayTab(5)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="5">Jumat</button>
|
||||
<button onclick="switchDayTab(6)" class="day-tab px-3 py-2 rounded text-sm font-medium"
|
||||
data-day="6">Sabtu</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
@@ -610,7 +659,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div id="emptyJadwal" class="hidden p-8 text-center text-slate-400">
|
||||
Belum ada jadwal.
|
||||
Belum ada jadwal untuk hari ini.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -629,14 +678,33 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-xl shadow">
|
||||
<h3 class="font-semibold text-lg mb-4">📅 Hari Libur</h3>
|
||||
<div class="flex items-center gap-3 bg-slate-50 p-3 rounded border">
|
||||
<h3 class="font-semibold text-lg mb-4">📅 Mode Jadwal</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2 text-slate-700">Pilih Mode:</label>
|
||||
<select id="scheduleMode" class="w-full border rounded p-2" onchange="onScheduleModeChange()">
|
||||
<option value="0">1 Hari (Sama Setiap Hari)</option>
|
||||
<option value="1">7 Hari (Berbeda Tiap Hari)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="skipSundayContainer" class="flex items-center gap-3 bg-slate-50 p-3 rounded border mb-3">
|
||||
<input type="checkbox" id="skipSunday" style="width: 1.25rem; height: 1.25rem;">
|
||||
<label for="skipSunday" class="font-medium cursor-pointer select-none text-slate-700">Libur hari Minggu
|
||||
(Bel mati)</label>
|
||||
</div>
|
||||
<div class="mt-3 text-right">
|
||||
<button onclick="saveSkipSunday()" class="btn btn-blue">Simpan</button>
|
||||
<p class="text-xs text-slate-500 mb-3" id="scheduleModeHint">Mode 1 Hari: Jadwal sama setiap hari. Centang
|
||||
opsi di atas untuk menonaktifkan bel di hari Minggu.</p>
|
||||
<div class="text-right">
|
||||
<button onclick="saveScheduleMode()" class="btn btn-blue">Simpan Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-xl shadow">
|
||||
<h3 class="font-semibold text-lg mb-4">🔔 Track Bel Manual</h3>
|
||||
<p class="text-sm text-slate-500 mb-3">Track MP3 yang diputar saat tombol TEST ditekan lama (2 detik)</p>
|
||||
<div class="flex gap-2">
|
||||
<input id="testTrack" type="number" min="1" max="9999" placeholder="1000" style="width: 100px;">
|
||||
<button onclick="saveTestTrack()" class="btn btn-blue">Simpan</button>
|
||||
<button onclick="previewTestTrack()" class="btn btn-green">▶ Test</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -746,6 +814,10 @@
|
||||
// --- State & Auth ---
|
||||
let jadwal = [];
|
||||
let isLoggedIn = false;
|
||||
let currentScheduleMode = 0; // 0 = 1-hari, 1 = 7-hari
|
||||
let currentDayTab = 1; // Default to Senin (1)
|
||||
const dayNames = ['M', 'S', 'S', 'R', 'K', 'J', 'S']; // Minggu, Senin, Selasa, Rabu, Kamis, Jumat, Sabtu
|
||||
const dayLabels = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
|
||||
async function login(event) {
|
||||
event.preventDefault();
|
||||
@@ -792,20 +864,50 @@
|
||||
function render() {
|
||||
const body = document.getElementById('body');
|
||||
const empty = document.getElementById('emptyJadwal');
|
||||
const dayTabsContainer = document.getElementById('dayTabsContainer');
|
||||
body.innerHTML = '';
|
||||
|
||||
if (!jadwal.length) {
|
||||
// Show/hide day tabs based on mode
|
||||
if (currentScheduleMode === 1) {
|
||||
dayTabsContainer.classList.remove('hidden');
|
||||
} else {
|
||||
dayTabsContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Filter jadwal based on mode
|
||||
let filteredJadwal = [];
|
||||
if (currentScheduleMode === 1) {
|
||||
// 7-day mode: show ALL schedules, mark which ones are active for this day
|
||||
jadwal.forEach((j, i) => {
|
||||
const days = j.days !== undefined ? j.days : 0x7F;
|
||||
const isActiveToday = (days & (1 << currentDayTab)) !== 0;
|
||||
filteredJadwal.push({ ...j, originalIndex: i, isActiveToday: isActiveToday });
|
||||
});
|
||||
} else {
|
||||
// 1-day mode: show all with global enabled
|
||||
filteredJadwal = jadwal.map((j, i) => ({ ...j, originalIndex: i, isActiveToday: j.enabled !== false }));
|
||||
}
|
||||
|
||||
if (!filteredJadwal.length) {
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
jadwal.forEach((j, i) => {
|
||||
filteredJadwal.forEach((j, displayIndex) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-slate-50';
|
||||
tr.dataset.originalIndex = j.originalIndex;
|
||||
|
||||
// In 7-day mode, checkbox controls per-day activation (days bit)
|
||||
// In 1-day mode, checkbox controls global enabled field
|
||||
const checkboxTitle = currentScheduleMode === 1
|
||||
? `Aktif di ${dayLabels[currentDayTab]}`
|
||||
: 'Enabled';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="text-center text-slate-500 col-index">${i + 1}</td>
|
||||
<td class="text-center"><input type="checkbox" ${j.enabled !== false ? 'checked' : ''} style="width:1.2rem; height:1.2rem;"></td>
|
||||
<td class="text-center text-slate-500 col-index">${displayIndex + 1}</td>
|
||||
<td class="text-center"><input type="checkbox" data-enabled ${j.isActiveToday ? 'checked' : ''} title="${checkboxTitle}" style="width:1.2rem; height:1.2rem;"></td>
|
||||
<td><input type="number" min="0" max="23" value="${j.jam}"></td>
|
||||
<td><input type="number" min="0" max="59" value="${j.menit}"></td>
|
||||
<td><input type="number" min="1" value="${j.track || j.trackStart || 1}"></td>
|
||||
@@ -813,49 +915,91 @@
|
||||
<td class="text-center">
|
||||
<div class="flex justify-center gap-1">
|
||||
<button onclick="preview(${j.track || j.trackStart || 1})" class="btn btn-green small">▶</button>
|
||||
<button onclick="del(${i})" class="btn btn-red small">✕</button>
|
||||
<button onclick="delByIndex(${j.originalIndex})" class="btn btn-red small">✕</button>
|
||||
</div>
|
||||
</td>`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function tambah() {
|
||||
if (!isLoggedIn) return;
|
||||
if (jadwal.length >= 20) return alert('Maksimal 20 jadwal');
|
||||
jadwal.push({ jam: 7, menit: 0, track: 1, desc: 'Bel Masuk', enabled: true });
|
||||
function switchDayTab(day) {
|
||||
currentDayTab = day;
|
||||
// Update active state
|
||||
document.querySelectorAll('.day-tab').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (parseInt(btn.dataset.day) === day) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function del(i) {
|
||||
function tambah() {
|
||||
if (!isLoggedIn) return;
|
||||
if (jadwal.length >= 20) return alert('Maksimal 20 jadwal');
|
||||
|
||||
// In 7-day mode, set only current day; in 1-day mode, set all days
|
||||
const days = currentScheduleMode === 1 ? (1 << currentDayTab) : 0x7F;
|
||||
jadwal.push({ jam: 7, menit: 0, track: 1, desc: 'Bel Masuk', enabled: true, days: days });
|
||||
render();
|
||||
}
|
||||
|
||||
function delByIndex(originalIndex) {
|
||||
if (!isLoggedIn) return;
|
||||
if (confirm('Hapus jadwal ini?')) {
|
||||
jadwal.splice(i, 1);
|
||||
jadwal.splice(originalIndex, 1);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep old del function for compatibility
|
||||
function del(i) {
|
||||
delByIndex(i);
|
||||
}
|
||||
|
||||
async function simpan() {
|
||||
if (!isLoggedIn) return;
|
||||
const rows = document.querySelectorAll('#body tr');
|
||||
const arr = [];
|
||||
rows.forEach(r => {
|
||||
const inputs = r.querySelectorAll('input');
|
||||
if (inputs.length >= 5) {
|
||||
arr.push({
|
||||
jam: parseInt(inputs[1].value || 0),
|
||||
menit: parseInt(inputs[2].value || 0),
|
||||
track: parseInt(inputs[3].value || 1),
|
||||
desc: inputs[4].value || '',
|
||||
enabled: inputs[0].checked
|
||||
});
|
||||
|
||||
// Update jadwal array based on displayed rows
|
||||
rows.forEach(tr => {
|
||||
const originalIndex = parseInt(tr.dataset.originalIndex);
|
||||
if (isNaN(originalIndex) || originalIndex >= jadwal.length) return;
|
||||
|
||||
const enabledCb = tr.querySelector('input[data-enabled]');
|
||||
const numInputs = tr.querySelectorAll('input[type="number"]');
|
||||
const textInput = tr.querySelector('input[type="text"]');
|
||||
|
||||
if (numInputs.length >= 3 && enabledCb) {
|
||||
jadwal[originalIndex].jam = parseInt(numInputs[0].value || 0);
|
||||
jadwal[originalIndex].menit = parseInt(numInputs[1].value || 0);
|
||||
jadwal[originalIndex].track = parseInt(numInputs[2].value || 1);
|
||||
jadwal[originalIndex].desc = textInput ? textInput.value : '';
|
||||
|
||||
if (currentScheduleMode === 1) {
|
||||
// 7-day mode: checkbox controls per-day bit in days field
|
||||
let currentDays = jadwal[originalIndex].days !== undefined ? jadwal[originalIndex].days : 0x7F;
|
||||
if (enabledCb.checked) {
|
||||
// Add current day bit
|
||||
currentDays |= (1 << currentDayTab);
|
||||
} else {
|
||||
// Remove current day bit
|
||||
currentDays &= ~(1 << currentDayTab);
|
||||
}
|
||||
jadwal[originalIndex].days = currentDays;
|
||||
// Keep enabled always true in 7-day mode (days field controls per-day)
|
||||
jadwal[originalIndex].enabled = true;
|
||||
} else {
|
||||
// 1-day mode: checkbox controls global enabled
|
||||
jadwal[originalIndex].enabled = enabledCb.checked;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await fetch('/api/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(arr)
|
||||
body: JSON.stringify(jadwal)
|
||||
});
|
||||
alert('✅ Jadwal Tersimpan!');
|
||||
loadJadwal();
|
||||
@@ -872,7 +1016,49 @@
|
||||
document.getElementById('mobileTitle').textContent = d.schoolName;
|
||||
}
|
||||
document.getElementById('skipSunday').checked = d.skipSunday || false;
|
||||
document.getElementById('testTrack').value = d.testTrack || 1000;
|
||||
if (d.user) document.getElementById('userInput').placeholder = d.user;
|
||||
|
||||
// Load schedule mode
|
||||
currentScheduleMode = d.scheduleMode || 0;
|
||||
document.getElementById('scheduleMode').value = currentScheduleMode;
|
||||
updateScheduleModeUI();
|
||||
render(); // Re-render to show/hide days column
|
||||
}
|
||||
|
||||
function updateScheduleModeUI() {
|
||||
const skipSundayContainer = document.getElementById('skipSundayContainer');
|
||||
const hint = document.getElementById('scheduleModeHint');
|
||||
|
||||
if (currentScheduleMode === 1) {
|
||||
// Mode 7-hari: hide skipSunday option
|
||||
skipSundayContainer.classList.add('hidden');
|
||||
hint.textContent = 'Mode 7 Hari: Pilih hari aktif untuk setiap jadwal di tabel Jadwal.';
|
||||
} else {
|
||||
// Mode 1-hari: show skipSunday option
|
||||
skipSundayContainer.classList.remove('hidden');
|
||||
hint.textContent = 'Mode 1 Hari: Jadwal sama setiap hari. Centang opsi di atas untuk menonaktifkan bel di hari Minggu.';
|
||||
}
|
||||
}
|
||||
|
||||
function onScheduleModeChange() {
|
||||
currentScheduleMode = parseInt(document.getElementById('scheduleMode').value);
|
||||
updateScheduleModeUI();
|
||||
render(); // Re-render to show/hide days column
|
||||
}
|
||||
|
||||
async function saveScheduleMode() {
|
||||
const mode = parseInt(document.getElementById('scheduleMode').value);
|
||||
const skip = document.getElementById('skipSunday').checked;
|
||||
|
||||
await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scheduleMode: mode, skipSunday: skip })
|
||||
});
|
||||
|
||||
currentScheduleMode = mode;
|
||||
alert('Mode Jadwal Disimpan!');
|
||||
}
|
||||
|
||||
async function saveName() {
|
||||
@@ -897,6 +1083,21 @@
|
||||
alert('Pengaturan Disimpan');
|
||||
}
|
||||
|
||||
async function saveTestTrack() {
|
||||
const track = parseInt(document.getElementById('testTrack').value) || 1000;
|
||||
await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ testTrack: track })
|
||||
});
|
||||
alert('Track Test Disimpan: ' + track);
|
||||
}
|
||||
|
||||
async function previewTestTrack() {
|
||||
const track = parseInt(document.getElementById('testTrack').value) || 1000;
|
||||
await fetch(`/api/play?track=${track}`);
|
||||
}
|
||||
|
||||
async function syncTime() {
|
||||
const now = new Date();
|
||||
const epoch = Math.floor((now.getTime() - now.getTimezoneOffset() * 60000) / 1000);
|
||||
|
||||
43
src/main.cpp
43
src/main.cpp
@@ -54,8 +54,12 @@ String adminUser = "admin";
|
||||
String adminPass = "sekolah123";
|
||||
String apName;
|
||||
bool skipSunday = false;
|
||||
uint16_t testTrack = 1000; // Default test track for manual bell
|
||||
uint8_t scheduleMode = 0; // 0 = 1-hari (seragam), 1 = 7-hari (per hari)
|
||||
|
||||
struct Sch { uint8_t jam; uint8_t menit; uint16_t track; String desc; bool enabled; uint32_t lastExecuted; };
|
||||
// days: bit-field untuk hari aktif (bit 0=Minggu, 1=Senin, 2=Selasa, 3=Rabu, 4=Kamis, 5=Jumat, 6=Sabtu)
|
||||
// Default 0x7F = semua hari aktif (0111 1111)
|
||||
struct Sch { uint8_t jam; uint8_t menit; uint16_t track; String desc; bool enabled; uint8_t days; uint32_t lastExecuted; };
|
||||
Sch schedules[MAX_SCHEDULES];
|
||||
uint8_t scheduleCount = 0;
|
||||
|
||||
@@ -147,6 +151,8 @@ void loadConfig() {
|
||||
d["user"] = adminUser;
|
||||
d["pass"] = adminPass;
|
||||
d["skipSunday"] = skipSunday;
|
||||
d["testTrack"] = testTrack;
|
||||
d["scheduleMode"] = scheduleMode;
|
||||
File f = LittleFS.open("/config.json", "w");
|
||||
if (f) { serializeJson(d, f); f.close(); }
|
||||
return;
|
||||
@@ -161,6 +167,8 @@ void loadConfig() {
|
||||
if (d.containsKey("user")) adminUser = d["user"].as<String>();
|
||||
if (d.containsKey("pass")) adminPass = d["pass"].as<String>();
|
||||
if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"];
|
||||
if (d.containsKey("testTrack")) testTrack = d["testTrack"] | 1000;
|
||||
if (d.containsKey("scheduleMode")) scheduleMode = d["scheduleMode"] | 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +178,8 @@ void saveConfig() {
|
||||
d["user"] = adminUser;
|
||||
d["pass"] = adminPass;
|
||||
d["skipSunday"] = skipSunday;
|
||||
d["testTrack"] = testTrack;
|
||||
d["scheduleMode"] = scheduleMode;
|
||||
File f = LittleFS.open("/config.json", "w");
|
||||
if (f) { serializeJson(d, f); f.close(); }
|
||||
}
|
||||
@@ -197,6 +207,7 @@ void loadSchedules() {
|
||||
schedules[i].track = arr[i]["track"] | 1;
|
||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||
schedules[i].days = arr[i]["days"] | 0x7F; // default semua hari aktif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +221,7 @@ void saveSchedules() {
|
||||
o["track"] = schedules[i].track;
|
||||
o["desc"] = schedules[i].desc;
|
||||
o["enabled"] = schedules[i].enabled;
|
||||
o["days"] = schedules[i].days;
|
||||
}
|
||||
File f = LittleFS.open("/schedule.json", "w");
|
||||
if (f) { serializeJson(doc, f); f.close(); }
|
||||
@@ -319,6 +331,7 @@ void initWebServer() {
|
||||
o["track"] = schedules[i].track;
|
||||
o["desc"] = schedules[i].desc;
|
||||
o["enabled"] = schedules[i].enabled;
|
||||
o["days"] = schedules[i].days;
|
||||
}
|
||||
String out; serializeJson(d, out);
|
||||
req->send(200, "application/json", out);
|
||||
@@ -339,6 +352,7 @@ void initWebServer() {
|
||||
schedules[i].track = arr[i]["track"] | 1;
|
||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||
}
|
||||
saveSchedules();
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
@@ -351,6 +365,8 @@ void initWebServer() {
|
||||
d["schoolName"] = schoolName;
|
||||
d["user"] = adminUser;
|
||||
d["skipSunday"] = skipSunday;
|
||||
d["testTrack"] = testTrack;
|
||||
d["scheduleMode"] = scheduleMode;
|
||||
String out; serializeJson(d, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
@@ -362,6 +378,8 @@ void initWebServer() {
|
||||
if (deserializeJson(d, data, len)) { req->send(400); return; }
|
||||
if (d.containsKey("schoolName")) schoolName = d["schoolName"].as<String>();
|
||||
if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"];
|
||||
if (d.containsKey("testTrack")) testTrack = d["testTrack"] | 1000;
|
||||
if (d.containsKey("scheduleMode")) scheduleMode = d["scheduleMode"] | 0;
|
||||
saveConfig();
|
||||
req->send(200, "application/json", "{\"ok\":true}");
|
||||
broadcastStatus();
|
||||
@@ -423,6 +441,7 @@ void initWebServer() {
|
||||
doc["user"] = adminUser;
|
||||
doc["pass"] = adminPass;
|
||||
doc["skipSunday"] = skipSunday;
|
||||
doc["scheduleMode"] = scheduleMode;
|
||||
JsonArray arr = doc.createNestedArray("schedules");
|
||||
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
||||
JsonObject o = arr.createNestedObject();
|
||||
@@ -431,6 +450,7 @@ void initWebServer() {
|
||||
o["track"] = schedules[i].track;
|
||||
o["desc"] = schedules[i].desc;
|
||||
o["enabled"] = schedules[i].enabled;
|
||||
o["days"] = schedules[i].days;
|
||||
}
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
@@ -473,6 +493,7 @@ void initWebServer() {
|
||||
if (doc.containsKey("schoolName")) schoolName = doc["schoolName"].as<String>();
|
||||
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
||||
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
||||
if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0;
|
||||
saveConfig();
|
||||
// Update schedules
|
||||
if (doc.containsKey("schedules")) {
|
||||
@@ -485,6 +506,7 @@ void initWebServer() {
|
||||
schedules[i].track = arr[i]["track"] | 1;
|
||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||
}
|
||||
saveSchedules();
|
||||
}
|
||||
@@ -533,6 +555,7 @@ void initWebServer() {
|
||||
doc["user"] = adminUser;
|
||||
doc["pass"] = adminPass;
|
||||
doc["skipSunday"] = skipSunday;
|
||||
doc["scheduleMode"] = scheduleMode;
|
||||
JsonArray arr = doc.createNestedArray("schedules");
|
||||
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
||||
JsonObject o = arr.createNestedObject();
|
||||
@@ -541,6 +564,7 @@ void initWebServer() {
|
||||
o["track"] = schedules[i].track;
|
||||
o["desc"] = schedules[i].desc;
|
||||
o["enabled"] = schedules[i].enabled;
|
||||
o["days"] = schedules[i].days;
|
||||
}
|
||||
File f = LittleFS.open(filename, "w");
|
||||
if (f) {
|
||||
@@ -587,6 +611,7 @@ void initWebServer() {
|
||||
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
||||
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
||||
if (doc.containsKey("skipSunday")) skipSunday = doc["skipSunday"];
|
||||
if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0;
|
||||
saveConfig();
|
||||
|
||||
if (doc.containsKey("schedules")) {
|
||||
@@ -599,6 +624,7 @@ void initWebServer() {
|
||||
schedules[i].track = arr[i]["track"] | 1;
|
||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||
}
|
||||
saveSchedules();
|
||||
}
|
||||
@@ -798,8 +824,8 @@ void loop(){
|
||||
// Process TEST_BUTTON long press: play test MP3
|
||||
if (testBtnLongPressPending) {
|
||||
testBtnLongPressPending = false;
|
||||
Serial.println("[BUTTON] Long press detected: playing track 1000");
|
||||
playTrack(1000, "Manual Test");
|
||||
Serial.printf("[BUTTON] Long press detected: playing test track %d\n", testTrack);
|
||||
playTrack(testTrack, "Manual Test");
|
||||
}
|
||||
|
||||
// Process RESET_BUTTON long press: reset admin credentials
|
||||
@@ -851,9 +877,18 @@ void loop(){
|
||||
|
||||
// schedule loop: for each schedule, compute today's target unix
|
||||
if (now.year() >= 2025) { // only run schedules if date >= 2025
|
||||
uint8_t today = now.dayOfTheWeek(); // 0=Minggu, 1=Senin, ..., 6=Sabtu
|
||||
for (uint8_t i=0;i<scheduleCount;i++){
|
||||
if (!schedules[i].enabled) continue; // skip disabled schedules
|
||||
if (skipSunday && now.dayOfTheWeek() == 0) continue; // skip on Sunday if enabled
|
||||
|
||||
// Check day based on schedule mode
|
||||
if (scheduleMode == 1) {
|
||||
// Mode 7-hari: cek apakah hari ini aktif untuk jadwal ini
|
||||
if (!(schedules[i].days & (1 << today))) continue;
|
||||
} else {
|
||||
// Mode 1-hari (seragam): skip on Sunday if skipSunday enabled
|
||||
if (skipSunday && today == 0) continue;
|
||||
}
|
||||
DateTime target(now.year(), now.month(), now.day(), schedules[i].jam, schedules[i].menit, 0);
|
||||
uint32_t tUnix = target.unixtime();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user