Files
bel-sekolah-esp32/data/setting.html

1041 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pengaturan — Bel Sekolah</title>
<style>
/* Reset & Base */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f1f5f9;
color: #334155;
}
/* Layout Utilities */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.h-screen {
height: 100vh;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-x-auto {
overflow-x: auto;
}
.flex-1 {
flex: 1;
}
.w-64 {
width: 16rem;
}
.w-full {
width: 100%;
}
.hidden {
display: none !important;
}
.block {
display: block;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.fixed {
position: fixed;
}
.inset-0 {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.z-50 {
z-index: 50;
}
.z-40 {
z-index: 40;
}
.z-10 {
z-index: 10;
}
/* Spacing & Sizing */
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mt-4 {
margin-top: 1rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
/* Hide Spinners */
input[type=number] {
text-align: center;
}
/* Force min-width on inputs for mobile */
@media (max-width: 768px) {
/* Hide Spinners on Mobile only */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
min-width: 35px;
}
th,
td {
font-size: 0.75rem;
padding: 0.2rem;
}
}
/* Typography */
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.text-sm {
font-size: 0.875rem;
}
.text-xs {
font-size: 0.75rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
}
.text-center {
text-align: center;
}
.text-white {
color: white;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #16a34a;
}
.text-red-500 {
color: #ef4444;
}
.text-slate-300 {
color: #cbd5e1;
}
.text-slate-400 {
color: #94a3b8;
}
.text-slate-500 {
color: #64748b;
}
.text-slate-600 {
color: #475569;
}
.text-slate-700 {
color: #334155;
}
.text-slate-800 {
color: #1e293b;
}
/* Components */
.bg-white {
background-color: white;
}
.bg-slate-800 {
background-color: #1e293b;
}
.bg-slate-900 {
background-color: #0f172a;
}
.bg-slate-100 {
background-color: #f1f5f9;
}
.bg-slate-50 {
background-color: #f8fafc;
}
.shadow {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.rounded {
border-radius: 0.375rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.border {
border: 1px solid #cbd5e1;
}
.border-b {
border-bottom: 1px solid #e2e8f0;
}
.border-t {
border-top: 1px solid #e2e8f0;
}
.cursor-pointer {
cursor: pointer;
}
.hover\:bg-slate-50:hover {
background-color: #f8fafc;
}
/* Sidebar */
.sidebar {
transition: transform 0.3s ease-in-out;
}
.sidebar-link {
display: block;
padding: 0.75rem 1rem;
color: #cbd5e1;
text-decoration: none;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
transition: background-color 0.2s, color 0.2s;
cursor: pointer;
}
.sidebar-link:hover {
background-color: #334155;
color: white;
}
.sidebar-link.active {
background-color: #2563eb;
color: white;
}
/* Buttons */
input[type="text"],
input[type="password"],
input[type="number"] {
width: 100%;
border: 1px solid #cbd5e1;
padding: 0.5rem;
border-radius: 0.375rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
border: none;
color: white;
text-decoration: none;
text-align: center;
}
.btn.small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.btn-blue {
background-color: #2563eb;
}
.btn-blue:hover {
background-color: #1d4ed8;
}
.btn-green {
background-color: #16a34a;
}
.btn-green:hover {
background-color: #15803d;
}
.btn-red {
background-color: #dc2626;
}
.btn-red:hover {
background-color: #b91c1c;
}
.btn-gray {
background-color: #64748b;
}
.btn-gray:hover {
background-color: #475569;
}
.btn-purple {
background-color: #7c3aed;
}
.btn-purple:hover {
background-color: #6d28d9;
}
/* Table */
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
background-color: #f1f5f9;
padding: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
}
td {
padding: 0.5rem;
border-bottom: 1px solid #f1f5f9;
}
/* Mobile Responsive */
.mobile-header {
display: none;
}
@media (max-width: 768px) {
.layout-container {
flex-direction: column;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 50;
width: 16rem;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.content-area {
padding: 1rem;
}
.mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background-color: #1e293b;
color: white;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 40;
}
.overlay.open {
display: block;
}
/* Table adjust */
/* Force min-width on inputs for mobile */
input[type="number"] {
min-width: 35px;
padding: 0.1rem;
}
th,
td {
font-size: 0.75rem;
padding: 0.1rem;
}
.th-desc {
min-width: 50px;
}
.col-index {
display: none;
}
.btn.small {
padding: 0.15rem 0.3rem;
font-size: 0.75rem;
}
}
/* Desktop default */
.th-desc {
min-width: 150px;
}
</style>
</head>
<body>
<!-- Login Overlay -->
<div id="loginOverlay" class="h-screen w-full flex items-center justify-center bg-slate-50 absolute z-50">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
<div class="bg-white p-6 rounded-xl shadow w-full max-w-sm mx-4">
<h1 class="text-2xl font-bold text-center text-blue-600 mb-6">🔐 Login Admin</h1>
<form onsubmit="login(event)">
<div class="mb-4">
<label class="block text-sm font-medium mb-1 text-slate-700">Username</label>
<input id="loginUser" type="text" placeholder="admin" required>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-1 text-slate-700">Password</label>
<input id="loginPass" type="password" placeholder="******" required>
</div>
<button type="submit" class="btn btn-blue w-full">Masuk Dashboard</button>
</form>
<div id="loginError" class="mt-4 text-center text-red-500 text-sm hidden"></div>
</div>
</div>
</div>
<!-- Dashboard Layout -->
<div id="dashboard" class="flex h-screen overflow-hidden layout-container hidden">
<!-- Mobile Header -->
<div class="mobile-header">
<div class="flex items-center gap-3">
<button onclick="toggleSidebar()" class="text-white text-2xl focus:outline-none"></button>
<span class="font-bold text-lg" id="mobileTitle">Bel Sekolah</span>
</div>
<div class="text-xs bg-green-600 px-2 py-1 rounded text-white hidden" id="statusBadge">Online</div>
</div>
<!-- Backdrop -->
<div id="sidebarOverlay" class="overlay" onclick="toggleSidebar()"></div>
<!-- Sidebar -->
<aside id="sidebar" class="sidebar w-64 bg-slate-800 text-slate-300 flex flex-col">
<div class="p-4 border-b border-slate-700 bg-slate-900 flex justify-between items-center">
<h1 class="text-xl font-bold text-white flex items-center gap-2">
🔔 <span id="sidebarTitle">Bel Sekolah</span>
</h1>
<!-- Close button for mobile -->
<button onclick="toggleSidebar()" class="text-slate-400 md:hidden text-xl"></button>
</div>
<nav class="flex-1 overflow-y-auto p-2">
<a onclick="switchTab('tab-jadwal')" class="sidebar-link active" id="link-tab-jadwal">📅 Jadwal Pelajaran</a>
<a onclick="switchTab('tab-config')" class="sidebar-link" id="link-tab-config">⚙️ Konfigurasi Umum</a>
<a onclick="switchTab('tab-backup')" class="sidebar-link" id="link-tab-backup">💾 Data & Backup</a>
<a onclick="switchTab('tab-admin')" class="sidebar-link" id="link-tab-admin">🔐 Akun Admin</a>
</nav>
<div class="p-4 border-t border-slate-700">
<div class="text-xs text-slate-500 mb-2">System Status</div>
<button onclick="stopBel()" class="btn btn-red w-full text-sm mb-2">⛔ Stop Bel</button>
<a href="/" class="btn btn-gray w-full text-sm text-center block">🏠 Home Page</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto bg-slate-50 p-6 content-area">
<div id="content-container" class="max-w-4xl mx-auto">
<!-- Tab: Jadwal -->
<div id="tab-jadwal" class="tab-content block">
<div class="flex justify-between items-center mb-6">
<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>
<button onclick="simpan()" class="btn btn-blue small">💾 Simpan</button>
</div>
</div>
<div class="bg-white rounded-xl shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr>
<th class="w-10 text-center col-index">#</th>
<th class="w-12 text-center">On</th>
<th class="w-10">Jam</th>
<th class="w-10">Mnt</th>
<th class="w-12">Trk</th>
<th class="th-desc">Deskripsi</th>
<th class="w-20 text-center">Aksi</th>
</tr>
</thead>
<tbody id="body"></tbody>
</table>
</div>
<div id="emptyJadwal" class="hidden p-8 text-center text-slate-400">
Belum ada jadwal.
</div>
</div>
</div>
<!-- Tab: Konfigurasi -->
<div id="tab-config" class="tab-content hidden">
<h2 class="text-2xl font-bold text-slate-800 mb-6">Konfigurasi</h2>
<div class="grid gap-6">
<div class="bg-white p-6 rounded-xl shadow">
<h3 class="font-semibold text-lg mb-4">🏫 Identitas Sekolah</h3>
<div class="flex gap-2">
<input id="schoolName" type="text" placeholder="Nama Sekolah..." class="flex-1">
<button onclick="saveName()" class="btn btn-blue">Simpan</button>
</div>
</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">
<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>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow">
<h3 class="font-semibold text-lg mb-4">🕒 Waktu Sistem</h3>
<button onclick="syncTime()" class="btn btn-purple">🕒 Sync Waktu Browser</button>
</div>
<div class="bg-white p-6 rounded-xl shadow">
<h3 class="font-semibold text-lg mb-4">📡 Update Firmware</h3>
<div id="otaInfo" class="text-sm text-slate-600">
Klik cek info untuk detail OTA.
</div>
<button onclick="loadOTAInfo()" class="btn btn-gray small mt-2">Cek Info OTA</button>
</div>
</div>
</div>
<!-- Tab: Backup -->
<div id="tab-backup" class="tab-content hidden">
<h2 class="text-2xl font-bold text-slate-800 mb-6">Backup Data</h2>
<div class="bg-white p-6 rounded-xl shadow mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-lg">💾 Backup Internal</h3>
<button onclick="saveInternalBackup()" class="btn btn-blue small"> Baru</button>
</div>
<div class="border rounded bg-slate-50">
<div id="backupList" class="divide-y max-h-64 overflow-y-auto">
<p class="p-4 text-center text-slate-400 text-sm">Memuat list...</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow">
<h3 class="font-semibold text-lg mb-4">💻 Ekspor / Impor JSON</h3>
<div class="flex gap-3">
<div class="flex-1 border rounded p-3 text-center hover:bg-slate-50 cursor-pointer" onclick="backup()">
<div class="text-2xl mb-1">⬇️</div>
<div class="font-bold text-blue-600 text-sm">Download</div>
</div>
<div class="flex-1 border rounded p-3 text-center hover:bg-slate-50 cursor-pointer"
onclick="document.getElementById('restoreFile').click()">
<div class="text-2xl mb-1">⬆️</div>
<div class="font-bold text-green-600 text-sm">Upload</div>
<input type="file" id="restoreFile" accept=".json" class="hidden" onchange="restoreFileSelected(this)">
</div>
</div>
</div>
</div>
<!-- Tab: Admin -->
<div id="tab-admin" class="tab-content hidden">
<h2 class="text-2xl font-bold text-slate-800 mb-6">Ganti Password</h2>
<div class="bg-white p-6 rounded-xl shadow max-w-lg">
<div class="mb-4">
<label class="block text-sm font-medium mb-1 text-slate-700">Username Baru</label>
<input id="userInput" type="text">
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-1 text-slate-700">Password Baru</label>
<input id="passInput" type="password">
</div>
<button onclick="changeAdmin()" class="btn btn-purple w-full">💾 Simpan Perubahan</button>
<div class="mt-8 pt-4 border-t text-center">
<a href="https://wa.me/628113936644?text=Perlu%20Bantuan%3F" target="_blank"
class="text-green-600 font-medium hover:underline text-sm">
💬 Hubungi Bantuan (WhatsApp)
</a>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
// --- UI Logic ---
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
}
// Auto close sidebar on mobile when link clicked
function switchTab(tabId) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('block'));
document.getElementById(tabId).classList.remove('hidden');
document.getElementById(tabId).classList.add('block');
document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
document.getElementById('link-' + tabId).classList.add('active');
if (window.innerWidth <= 768) {
toggleSidebar(); // Close menu
}
if (tabId === 'tab-backup') loadBackupList();
}
// --- State & Auth ---
let jadwal = [];
let isLoggedIn = false;
async function login(event) {
event.preventDefault();
const user = document.getElementById('loginUser').value.trim();
const pass = document.getElementById('loginPass').value.trim();
if (!user || !pass) return showLoginError('Masukkan username & password');
try {
const res = await fetch('/api/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user, pass })
});
const result = await res.json();
if (result.valid) {
isLoggedIn = true;
document.getElementById('loginOverlay').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
loadJadwal();
loadConfig();
} else {
showLoginError('Username atau password salah!');
}
} catch (e) { showLoginError('Gagal terhubung ke perangkat. Coba refresh.'); }
}
function showLoginError(msg) {
const el = document.getElementById('loginError');
el.textContent = msg;
el.classList.remove('hidden');
}
// --- Data Rendering ---
async function loadJadwal() {
if (!isLoggedIn) return;
const r = await fetch('/api/jadwal');
const d = await r.json();
jadwal = d || [];
render();
}
function render() {
const body = document.getElementById('body');
const empty = document.getElementById('emptyJadwal');
body.innerHTML = '';
if (!jadwal.length) {
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
jadwal.forEach((j, i) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-slate-50';
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><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>
<td><input type="text" value="${j.desc || ''}" placeholder="Ket..."></td>
<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>
</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 });
render();
}
function del(i) {
if (!isLoggedIn) return;
if (confirm('Hapus jadwal ini?')) {
jadwal.splice(i, 1);
render();
}
}
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
});
}
});
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arr)
});
alert('✅ Jadwal Tersimpan!');
loadJadwal();
}
// --- API Calls ---
async function loadConfig() {
if (!isLoggedIn) return;
const r = await fetch('/api/config');
const d = await r.json();
if (d.schoolName) {
document.getElementById('schoolName').value = d.schoolName;
document.getElementById('sidebarTitle').textContent = d.schoolName;
document.getElementById('mobileTitle').textContent = d.schoolName;
}
document.getElementById('skipSunday').checked = d.skipSunday || false;
if (d.user) document.getElementById('userInput').placeholder = d.user;
}
async function saveName() {
const name = document.getElementById('schoolName').value.trim();
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ schoolName: name })
});
document.getElementById('sidebarTitle').textContent = name;
document.getElementById('mobileTitle').textContent = name;
alert('Nama Sekolah Disimpan');
}
async function saveSkipSunday() {
const skip = document.getElementById('skipSunday').checked;
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ skipSunday: skip })
});
alert('Pengaturan Disimpan');
}
async function syncTime() {
const now = new Date();
const epoch = Math.floor((now.getTime() - now.getTimezoneOffset() * 60000) / 1000);
await fetch('/api/settime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ epoch })
});
alert('✅ Waktu tersinkronisasi!');
}
async function changeAdmin() {
const u = document.getElementById('userInput').value.trim();
const p = document.getElementById('passInput').value.trim();
if (!u || !p) return alert('Isi user & pass baru');
const res = await fetch('/api/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: u, pass: p })
});
if (res.ok) {
alert('Login diganti. Login ulang.');
location.reload();
} else alert('Gagal');
}
async function preview(track) { await fetch(`/api/play?track=${track}`); }
async function stopBel() { await fetch('/api/stop'); }
async function loadOTAInfo() {
try {
const r = await fetch('/api/ota');
const d = await r.json();
document.getElementById('otaInfo').innerHTML = `Host: <b class="font-mono">${d.hostname}</b><br>Pass: <b class="font-mono">${d.password}</b>`;
} catch (e) { alert('Gagal load info'); }
}
// --- Backup Logic ---
async function loadBackupList() {
const listDiv = document.getElementById('backupList');
try {
const res = await fetch('/api/backup/list');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
let files;
try { files = JSON.parse(text); } catch (e) { files = []; }
if (!Array.isArray(files) || files.length === 0) {
listDiv.innerHTML = '<div class="p-4 text-center text-slate-400 text-sm">Kosong.</div>';
return;
}
files.sort((a, b) => b.name.localeCompare(a.name));
let html = '';
files.forEach(f => {
const nameDisplay = f.name.replace('/backup_', '').replace('.json', '').replace('_', ' ');
html += `
<div class="flex items-center justify-between p-3 hover:bg-slate-50 group">
<div class="flex items-center gap-3 overflow-hidden">
<div class="text-lg">📄</div>
<div class="min-w-0">
<div class="font-medium text-sm text-slate-700 truncate">${nameDisplay}</div>
<div class="text-xs text-slate-400">${f.size || 0} b</div>
</div>
</div>
<div class="flex gap-2">
<button onclick="loadInternal('${f.name}')" class="btn btn-blue small" title="Restore">♻️</button>
<button onclick="delInternal('${f.name}')" class="btn btn-red small" title="Hapus">✕</button>
</div>
</div>`;
});
listDiv.innerHTML = html;
} catch (e) {
listDiv.innerHTML = `<div class="p-4 text-center text-red-500 text-sm">Error: ${e.message}</div>`;
}
}
async function saveInternalBackup() {
if (!confirm('Backup sekarang?')) return;
try {
const res = await fetch('/api/backup/save', { method: 'POST' });
if (res.ok) { alert('✅ Backup OK'); loadBackupList(); }
else throw new Error('Failed');
} catch (e) { alert('Gagal backup'); }
}
async function loadInternal(fname) {
if (!confirm(`Restore dari ${fname}?`)) return;
try {
const res = await fetch('/api/backup/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: fname })
});
if (res.ok) { alert('✅ Restore OK. Reloading...'); location.reload(); }
} catch (e) { alert('Gagal restore'); }
}
async function delInternal(fname) {
if (!confirm(`Hapus ${fname}?`)) return;
try {
const res = await fetch('/api/backup/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: fname })
});
if (res.ok) loadBackupList();
} catch (e) { alert('Gagal hapus'); }
}
async function backup() {
try {
const res = await fetch('/api/backup/download');
const data = await res.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'backup.json';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
} catch (e) { alert('Gagal download'); }
}
async function restoreFileSelected(input) {
const file = input.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
const res = await fetch('/api/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) { alert('Restore OK!'); location.reload(); }
else throw new Error('API Error');
} catch (e) { alert('Gagal upload: ' + e.message); }
}
</script>
</body>
</html>