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 -->
|
<!-- data/index.html -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="id">
|
<html lang="id">
|
||||||
|
|
||||||
<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>Bel Sekolah</title>
|
<title>Bel Sekolah</title>
|
||||||
<style>
|
<style>
|
||||||
/* Inline Tailwind CSS for offline compatibility */
|
/* Inline Tailwind CSS for offline compatibility */
|
||||||
.bg-gradient-to-b { background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); }
|
.bg-gradient-to-b {
|
||||||
.from-slate-50 { --tw-gradient-from: #f8fafc; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(248, 250, 252, 0)); }
|
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
|
||||||
.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; }
|
.from-slate-50 {
|
||||||
.text-slate-800 { --tw-text-opacity: 1; color: rgb(30 41 59 / var(--tw-text-opacity)); }
|
--tw-gradient-from: #f8fafc;
|
||||||
.max-w-2xl { max-width: 42rem; }
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(248, 250, 252, 0));
|
||||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
}
|
||||||
.p-6 { padding: 1.5rem; }
|
|
||||||
.text-center { text-align: center; }
|
.to-white {
|
||||||
.mb-6 { margin-bottom: 1.5rem; }
|
--tw-gradient-to: #ffffff;
|
||||||
.text-5xl { font-size: 3rem; line-height: 1; }
|
}
|
||||||
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
|
||||||
.font-bold { font-weight: 700; }
|
.min-h-screen {
|
||||||
.text-blue-600 { --tw-text-opacity: 1; color: rgb(37 99 235 / var(--tw-text-opacity)); }
|
min-height: 100vh;
|
||||||
.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; }
|
.font-sans {
|
||||||
.bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); }
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
.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-slate-800 {
|
||||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
--tw-text-opacity: 1;
|
||||||
.mb-1 { margin-bottom: 0.25rem; }
|
color: rgb(30 41 59 / var(--tw-text-opacity));
|
||||||
.text-5xl { font-size: 3rem; line-height: 1; }
|
}
|
||||||
.font-extrabold { font-weight: 800; }
|
|
||||||
.tracking-wider { letter-spacing: 0.05em; }
|
.max-w-2xl {
|
||||||
.text-slate-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); }
|
max-width: 42rem;
|
||||||
.mt-2 { margin-top: 0.5rem; }
|
}
|
||||||
.flex { display: flex; }
|
|
||||||
.items-center { align-items: center; }
|
.mx-auto {
|
||||||
.justify-center { justify-content: center; }
|
margin-left: auto;
|
||||||
.gap-4 { gap: 1rem; }
|
margin-right: auto;
|
||||||
.my-3 { margin-top: 0.75rem; margin-bottom: 0.75rem; }
|
}
|
||||||
.w-3 { width: 0.75rem; }
|
|
||||||
.h-3 { height: 0.75rem; }
|
.p-6 {
|
||||||
.rounded-full { border-radius: 9999px; }
|
padding: 1.5rem;
|
||||||
.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; }
|
.text-center {
|
||||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
text-align: center;
|
||||||
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
}
|
||||||
.gap-3 { gap: 0.75rem; }
|
|
||||||
.mt-6 { margin-top: 1.5rem; }
|
.mb-6 {
|
||||||
.max-w-md { max-width: 28rem; }
|
margin-bottom: 1.5rem;
|
||||||
.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; }
|
.text-5xl {
|
||||||
.h-40 { height: 10rem; }
|
font-size: 3rem;
|
||||||
.overflow-auto { overflow: auto; }
|
line-height: 1;
|
||||||
.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; }
|
.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 */
|
/* Button styles */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -71,19 +251,46 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
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.small {
|
||||||
.btn.btn-green { background: #059669; }
|
padding: 0.25rem 0.5rem;
|
||||||
.btn.btn-red { background: #DC2626; }
|
font-size: 0.9rem;
|
||||||
.btn.btn-gray { background: #6B7280; }
|
}
|
||||||
.btn.btn-purple { background: #7C3AED; }
|
|
||||||
.btn:hover { filter: brightness(0.95); }
|
.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 */
|
/* Responsive */
|
||||||
@media (max-width: 640px) {
|
@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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gradient-to-b from-slate-50 to-white min-h-screen font-sans text-slate-800">
|
<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">
|
<div class="max-w-2xl mx-auto p-6">
|
||||||
<header class="text-center mb-6">
|
<header class="text-center mb-6">
|
||||||
@@ -120,93 +327,96 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// WebSocket connection (uses same host)
|
// WebSocket connection (uses same host)
|
||||||
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||||
const ws = new WebSocket(protocol + location.host + '/ws');
|
const ws = new WebSocket(protocol + location.host + '/ws');
|
||||||
|
|
||||||
const hariEl = document.getElementById('hari');
|
const hariEl = document.getElementById('hari');
|
||||||
const clockEl = document.getElementById('clock');
|
const clockEl = document.getElementById('clock');
|
||||||
const tanggalEl = document.getElementById('tanggal');
|
const tanggalEl = document.getElementById('tanggal');
|
||||||
const statusDot = document.getElementById('statusDot');
|
const statusDot = document.getElementById('statusDot');
|
||||||
const statusText = document.getElementById('statusText');
|
const statusText = document.getElementById('statusText');
|
||||||
const schoolEl = document.getElementById('schoolName');
|
const schoolEl = document.getElementById('schoolName');
|
||||||
const logEl = document.getElementById('log');
|
const logEl = document.getElementById('log');
|
||||||
|
|
||||||
function appendLog(s) {
|
function appendLog(s) {
|
||||||
const t = document.createElement('div');
|
const t = document.createElement('div');
|
||||||
t.textContent = (new Date()).toLocaleTimeString() + " — " + s;
|
t.textContent = (new Date()).toLocaleTimeString() + " — " + s;
|
||||||
logEl.prepend(t);
|
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);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Buttons
|
ws.onopen = () => {
|
||||||
document.getElementById('btnTest').addEventListener('click', async () => {
|
appendLog("🔌 WebSocket tersambung.");
|
||||||
// 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); }
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnStop').addEventListener('click', async () => {
|
ws.onclose = () => {
|
||||||
try {
|
appendLog("⚠️ WebSocket terputus.");
|
||||||
await fetch('/api/stop');
|
statusDot.style.background = 'gray';
|
||||||
} catch(e) { console.error(e); }
|
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>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@@ -515,6 +515,35 @@
|
|||||||
.th-desc {
|
.th-desc {
|
||||||
min-width: 150px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -584,7 +613,7 @@
|
|||||||
|
|
||||||
<!-- Tab: Jadwal -->
|
<!-- Tab: Jadwal -->
|
||||||
<div id="tab-jadwal" class="tab-content block">
|
<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>
|
<h2 class="text-2xl font-bold text-slate-800">Jadwal</h2>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button onclick="tambah()" class="btn btn-green small">➕ Tambah</button>
|
<button onclick="tambah()" class="btn btn-green small">➕ Tambah</button>
|
||||||
@@ -592,6 +621,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="bg-white rounded-xl shadow overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
@@ -610,7 +659,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div id="emptyJadwal" class="hidden p-8 text-center text-slate-400">
|
<div id="emptyJadwal" class="hidden p-8 text-center text-slate-400">
|
||||||
Belum ada jadwal.
|
Belum ada jadwal untuk hari ini.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -629,14 +678,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-6 rounded-xl shadow">
|
<div class="bg-white p-6 rounded-xl shadow">
|
||||||
<h3 class="font-semibold text-lg mb-4">📅 Hari Libur</h3>
|
<h3 class="font-semibold text-lg mb-4">📅 Mode Jadwal</h3>
|
||||||
<div class="flex items-center gap-3 bg-slate-50 p-3 rounded border">
|
<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;">
|
<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
|
<label for="skipSunday" class="font-medium cursor-pointer select-none text-slate-700">Libur hari Minggu
|
||||||
(Bel mati)</label>
|
(Bel mati)</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-right">
|
<p class="text-xs text-slate-500 mb-3" id="scheduleModeHint">Mode 1 Hari: Jadwal sama setiap hari. Centang
|
||||||
<button onclick="saveSkipSunday()" class="btn btn-blue">Simpan</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -746,6 +814,10 @@
|
|||||||
// --- State & Auth ---
|
// --- State & Auth ---
|
||||||
let jadwal = [];
|
let jadwal = [];
|
||||||
let isLoggedIn = false;
|
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) {
|
async function login(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -792,20 +864,50 @@
|
|||||||
function render() {
|
function render() {
|
||||||
const body = document.getElementById('body');
|
const body = document.getElementById('body');
|
||||||
const empty = document.getElementById('emptyJadwal');
|
const empty = document.getElementById('emptyJadwal');
|
||||||
|
const dayTabsContainer = document.getElementById('dayTabsContainer');
|
||||||
body.innerHTML = '';
|
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');
|
empty.classList.remove('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
empty.classList.add('hidden');
|
empty.classList.add('hidden');
|
||||||
|
|
||||||
jadwal.forEach((j, i) => {
|
filteredJadwal.forEach((j, displayIndex) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.className = 'hover:bg-slate-50';
|
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 = `
|
tr.innerHTML = `
|
||||||
<td class="text-center text-slate-500 col-index">${i + 1}</td>
|
<td class="text-center text-slate-500 col-index">${displayIndex + 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"><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="23" value="${j.jam}"></td>
|
||||||
<td><input type="number" min="0" max="59" value="${j.menit}"></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>
|
<td><input type="number" min="1" value="${j.track || j.trackStart || 1}"></td>
|
||||||
@@ -813,49 +915,91 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="flex justify-center gap-1">
|
<div class="flex justify-center gap-1">
|
||||||
<button onclick="preview(${j.track || j.trackStart || 1})" class="btn btn-green small">▶</button>
|
<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>
|
</div>
|
||||||
</td>`;
|
</td>`;
|
||||||
body.appendChild(tr);
|
body.appendChild(tr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function tambah() {
|
function switchDayTab(day) {
|
||||||
if (!isLoggedIn) return;
|
currentDayTab = day;
|
||||||
if (jadwal.length >= 20) return alert('Maksimal 20 jadwal');
|
// Update active state
|
||||||
jadwal.push({ jam: 7, menit: 0, track: 1, desc: 'Bel Masuk', enabled: true });
|
document.querySelectorAll('.day-tab').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
if (parseInt(btn.dataset.day) === day) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
render();
|
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 (!isLoggedIn) return;
|
||||||
if (confirm('Hapus jadwal ini?')) {
|
if (confirm('Hapus jadwal ini?')) {
|
||||||
jadwal.splice(i, 1);
|
jadwal.splice(originalIndex, 1);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep old del function for compatibility
|
||||||
|
function del(i) {
|
||||||
|
delByIndex(i);
|
||||||
|
}
|
||||||
|
|
||||||
async function simpan() {
|
async function simpan() {
|
||||||
if (!isLoggedIn) return;
|
if (!isLoggedIn) return;
|
||||||
const rows = document.querySelectorAll('#body tr');
|
const rows = document.querySelectorAll('#body tr');
|
||||||
const arr = [];
|
|
||||||
rows.forEach(r => {
|
// Update jadwal array based on displayed rows
|
||||||
const inputs = r.querySelectorAll('input');
|
rows.forEach(tr => {
|
||||||
if (inputs.length >= 5) {
|
const originalIndex = parseInt(tr.dataset.originalIndex);
|
||||||
arr.push({
|
if (isNaN(originalIndex) || originalIndex >= jadwal.length) return;
|
||||||
jam: parseInt(inputs[1].value || 0),
|
|
||||||
menit: parseInt(inputs[2].value || 0),
|
const enabledCb = tr.querySelector('input[data-enabled]');
|
||||||
track: parseInt(inputs[3].value || 1),
|
const numInputs = tr.querySelectorAll('input[type="number"]');
|
||||||
desc: inputs[4].value || '',
|
const textInput = tr.querySelector('input[type="text"]');
|
||||||
enabled: inputs[0].checked
|
|
||||||
});
|
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', {
|
await fetch('/api/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(arr)
|
body: JSON.stringify(jadwal)
|
||||||
});
|
});
|
||||||
alert('✅ Jadwal Tersimpan!');
|
alert('✅ Jadwal Tersimpan!');
|
||||||
loadJadwal();
|
loadJadwal();
|
||||||
@@ -872,7 +1016,49 @@
|
|||||||
document.getElementById('mobileTitle').textContent = d.schoolName;
|
document.getElementById('mobileTitle').textContent = d.schoolName;
|
||||||
}
|
}
|
||||||
document.getElementById('skipSunday').checked = d.skipSunday || false;
|
document.getElementById('skipSunday').checked = d.skipSunday || false;
|
||||||
|
document.getElementById('testTrack').value = d.testTrack || 1000;
|
||||||
if (d.user) document.getElementById('userInput').placeholder = d.user;
|
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() {
|
async function saveName() {
|
||||||
@@ -897,6 +1083,21 @@
|
|||||||
alert('Pengaturan Disimpan');
|
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() {
|
async function syncTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const epoch = Math.floor((now.getTime() - now.getTimezoneOffset() * 60000) / 1000);
|
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 adminPass = "sekolah123";
|
||||||
String apName;
|
String apName;
|
||||||
bool skipSunday = false;
|
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];
|
Sch schedules[MAX_SCHEDULES];
|
||||||
uint8_t scheduleCount = 0;
|
uint8_t scheduleCount = 0;
|
||||||
|
|
||||||
@@ -147,6 +151,8 @@ void loadConfig() {
|
|||||||
d["user"] = adminUser;
|
d["user"] = adminUser;
|
||||||
d["pass"] = adminPass;
|
d["pass"] = adminPass;
|
||||||
d["skipSunday"] = skipSunday;
|
d["skipSunday"] = skipSunday;
|
||||||
|
d["testTrack"] = testTrack;
|
||||||
|
d["scheduleMode"] = scheduleMode;
|
||||||
File f = LittleFS.open("/config.json", "w");
|
File f = LittleFS.open("/config.json", "w");
|
||||||
if (f) { serializeJson(d, f); f.close(); }
|
if (f) { serializeJson(d, f); f.close(); }
|
||||||
return;
|
return;
|
||||||
@@ -161,6 +167,8 @@ void loadConfig() {
|
|||||||
if (d.containsKey("user")) adminUser = d["user"].as<String>();
|
if (d.containsKey("user")) adminUser = d["user"].as<String>();
|
||||||
if (d.containsKey("pass")) adminPass = d["pass"].as<String>();
|
if (d.containsKey("pass")) adminPass = d["pass"].as<String>();
|
||||||
if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"];
|
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["user"] = adminUser;
|
||||||
d["pass"] = adminPass;
|
d["pass"] = adminPass;
|
||||||
d["skipSunday"] = skipSunday;
|
d["skipSunday"] = skipSunday;
|
||||||
|
d["testTrack"] = testTrack;
|
||||||
|
d["scheduleMode"] = scheduleMode;
|
||||||
File f = LittleFS.open("/config.json", "w");
|
File f = LittleFS.open("/config.json", "w");
|
||||||
if (f) { serializeJson(d, f); f.close(); }
|
if (f) { serializeJson(d, f); f.close(); }
|
||||||
}
|
}
|
||||||
@@ -197,6 +207,7 @@ void loadSchedules() {
|
|||||||
schedules[i].track = arr[i]["track"] | 1;
|
schedules[i].track = arr[i]["track"] | 1;
|
||||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
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["track"] = schedules[i].track;
|
||||||
o["desc"] = schedules[i].desc;
|
o["desc"] = schedules[i].desc;
|
||||||
o["enabled"] = schedules[i].enabled;
|
o["enabled"] = schedules[i].enabled;
|
||||||
|
o["days"] = schedules[i].days;
|
||||||
}
|
}
|
||||||
File f = LittleFS.open("/schedule.json", "w");
|
File f = LittleFS.open("/schedule.json", "w");
|
||||||
if (f) { serializeJson(doc, f); f.close(); }
|
if (f) { serializeJson(doc, f); f.close(); }
|
||||||
@@ -319,6 +331,7 @@ void initWebServer() {
|
|||||||
o["track"] = schedules[i].track;
|
o["track"] = schedules[i].track;
|
||||||
o["desc"] = schedules[i].desc;
|
o["desc"] = schedules[i].desc;
|
||||||
o["enabled"] = schedules[i].enabled;
|
o["enabled"] = schedules[i].enabled;
|
||||||
|
o["days"] = schedules[i].days;
|
||||||
}
|
}
|
||||||
String out; serializeJson(d, out);
|
String out; serializeJson(d, out);
|
||||||
req->send(200, "application/json", out);
|
req->send(200, "application/json", out);
|
||||||
@@ -339,6 +352,7 @@ void initWebServer() {
|
|||||||
schedules[i].track = arr[i]["track"] | 1;
|
schedules[i].track = arr[i]["track"] | 1;
|
||||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||||
|
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||||
}
|
}
|
||||||
saveSchedules();
|
saveSchedules();
|
||||||
req->send(200, "application/json", "{\"ok\":true}");
|
req->send(200, "application/json", "{\"ok\":true}");
|
||||||
@@ -351,6 +365,8 @@ void initWebServer() {
|
|||||||
d["schoolName"] = schoolName;
|
d["schoolName"] = schoolName;
|
||||||
d["user"] = adminUser;
|
d["user"] = adminUser;
|
||||||
d["skipSunday"] = skipSunday;
|
d["skipSunday"] = skipSunday;
|
||||||
|
d["testTrack"] = testTrack;
|
||||||
|
d["scheduleMode"] = scheduleMode;
|
||||||
String out; serializeJson(d, out);
|
String out; serializeJson(d, out);
|
||||||
req->send(200, "application/json", out);
|
req->send(200, "application/json", out);
|
||||||
});
|
});
|
||||||
@@ -362,6 +378,8 @@ void initWebServer() {
|
|||||||
if (deserializeJson(d, data, len)) { req->send(400); return; }
|
if (deserializeJson(d, data, len)) { req->send(400); return; }
|
||||||
if (d.containsKey("schoolName")) schoolName = d["schoolName"].as<String>();
|
if (d.containsKey("schoolName")) schoolName = d["schoolName"].as<String>();
|
||||||
if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"];
|
if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"];
|
||||||
|
if (d.containsKey("testTrack")) testTrack = d["testTrack"] | 1000;
|
||||||
|
if (d.containsKey("scheduleMode")) scheduleMode = d["scheduleMode"] | 0;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
req->send(200, "application/json", "{\"ok\":true}");
|
req->send(200, "application/json", "{\"ok\":true}");
|
||||||
broadcastStatus();
|
broadcastStatus();
|
||||||
@@ -423,6 +441,7 @@ void initWebServer() {
|
|||||||
doc["user"] = adminUser;
|
doc["user"] = adminUser;
|
||||||
doc["pass"] = adminPass;
|
doc["pass"] = adminPass;
|
||||||
doc["skipSunday"] = skipSunday;
|
doc["skipSunday"] = skipSunday;
|
||||||
|
doc["scheduleMode"] = scheduleMode;
|
||||||
JsonArray arr = doc.createNestedArray("schedules");
|
JsonArray arr = doc.createNestedArray("schedules");
|
||||||
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
||||||
JsonObject o = arr.createNestedObject();
|
JsonObject o = arr.createNestedObject();
|
||||||
@@ -431,6 +450,7 @@ void initWebServer() {
|
|||||||
o["track"] = schedules[i].track;
|
o["track"] = schedules[i].track;
|
||||||
o["desc"] = schedules[i].desc;
|
o["desc"] = schedules[i].desc;
|
||||||
o["enabled"] = schedules[i].enabled;
|
o["enabled"] = schedules[i].enabled;
|
||||||
|
o["days"] = schedules[i].days;
|
||||||
}
|
}
|
||||||
String out;
|
String out;
|
||||||
serializeJson(doc, out);
|
serializeJson(doc, out);
|
||||||
@@ -473,6 +493,7 @@ void initWebServer() {
|
|||||||
if (doc.containsKey("schoolName")) schoolName = doc["schoolName"].as<String>();
|
if (doc.containsKey("schoolName")) schoolName = doc["schoolName"].as<String>();
|
||||||
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
||||||
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
||||||
|
if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
// Update schedules
|
// Update schedules
|
||||||
if (doc.containsKey("schedules")) {
|
if (doc.containsKey("schedules")) {
|
||||||
@@ -485,6 +506,7 @@ void initWebServer() {
|
|||||||
schedules[i].track = arr[i]["track"] | 1;
|
schedules[i].track = arr[i]["track"] | 1;
|
||||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||||
|
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||||
}
|
}
|
||||||
saveSchedules();
|
saveSchedules();
|
||||||
}
|
}
|
||||||
@@ -533,6 +555,7 @@ void initWebServer() {
|
|||||||
doc["user"] = adminUser;
|
doc["user"] = adminUser;
|
||||||
doc["pass"] = adminPass;
|
doc["pass"] = adminPass;
|
||||||
doc["skipSunday"] = skipSunday;
|
doc["skipSunday"] = skipSunday;
|
||||||
|
doc["scheduleMode"] = scheduleMode;
|
||||||
JsonArray arr = doc.createNestedArray("schedules");
|
JsonArray arr = doc.createNestedArray("schedules");
|
||||||
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
for (uint8_t i = 0; i < scheduleCount; ++i) {
|
||||||
JsonObject o = arr.createNestedObject();
|
JsonObject o = arr.createNestedObject();
|
||||||
@@ -541,6 +564,7 @@ void initWebServer() {
|
|||||||
o["track"] = schedules[i].track;
|
o["track"] = schedules[i].track;
|
||||||
o["desc"] = schedules[i].desc;
|
o["desc"] = schedules[i].desc;
|
||||||
o["enabled"] = schedules[i].enabled;
|
o["enabled"] = schedules[i].enabled;
|
||||||
|
o["days"] = schedules[i].days;
|
||||||
}
|
}
|
||||||
File f = LittleFS.open(filename, "w");
|
File f = LittleFS.open(filename, "w");
|
||||||
if (f) {
|
if (f) {
|
||||||
@@ -587,6 +611,7 @@ void initWebServer() {
|
|||||||
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
if (doc.containsKey("user")) adminUser = doc["user"].as<String>();
|
||||||
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
if (doc.containsKey("pass")) adminPass = doc["pass"].as<String>();
|
||||||
if (doc.containsKey("skipSunday")) skipSunday = doc["skipSunday"];
|
if (doc.containsKey("skipSunday")) skipSunday = doc["skipSunday"];
|
||||||
|
if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|
||||||
if (doc.containsKey("schedules")) {
|
if (doc.containsKey("schedules")) {
|
||||||
@@ -599,6 +624,7 @@ void initWebServer() {
|
|||||||
schedules[i].track = arr[i]["track"] | 1;
|
schedules[i].track = arr[i]["track"] | 1;
|
||||||
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as<String>();
|
||||||
schedules[i].enabled = arr[i]["enabled"] | true;
|
schedules[i].enabled = arr[i]["enabled"] | true;
|
||||||
|
schedules[i].days = arr[i]["days"] | 0x7F;
|
||||||
}
|
}
|
||||||
saveSchedules();
|
saveSchedules();
|
||||||
}
|
}
|
||||||
@@ -798,8 +824,8 @@ void loop(){
|
|||||||
// Process TEST_BUTTON long press: play test MP3
|
// Process TEST_BUTTON long press: play test MP3
|
||||||
if (testBtnLongPressPending) {
|
if (testBtnLongPressPending) {
|
||||||
testBtnLongPressPending = false;
|
testBtnLongPressPending = false;
|
||||||
Serial.println("[BUTTON] Long press detected: playing track 1000");
|
Serial.printf("[BUTTON] Long press detected: playing test track %d\n", testTrack);
|
||||||
playTrack(1000, "Manual Test");
|
playTrack(testTrack, "Manual Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process RESET_BUTTON long press: reset admin credentials
|
// Process RESET_BUTTON long press: reset admin credentials
|
||||||
@@ -851,9 +877,18 @@ void loop(){
|
|||||||
|
|
||||||
// schedule loop: for each schedule, compute today's target unix
|
// schedule loop: for each schedule, compute today's target unix
|
||||||
if (now.year() >= 2025) { // only run schedules if date >= 2025
|
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++){
|
for (uint8_t i=0;i<scheduleCount;i++){
|
||||||
if (!schedules[i].enabled) continue; // skip disabled schedules
|
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);
|
DateTime target(now.year(), now.month(), now.day(), schedules[i].jam, schedules[i].menit, 0);
|
||||||
uint32_t tUnix = target.unixtime();
|
uint32_t tUnix = target.unixtime();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user