TKK_E32230273/indonesian_strings.json

636 lines
472 KiB
JSON

[
"Masuk",
"di browser untuk konfigurasi manual.",
"';\n } else {\n res.data.forEach(msg => {\n let isSelf = msg.sender === 'account';\n let cls = isSelf ? 'chat-right' : 'chat-left';\n let time = new Date(msg.created_at);\n let timeStr = (\"0\" + time.getHours()).slice(-2) + \":\" + (\"0\" + time.getMinutes()).slice(-2);\n let check = '';\n if (isSelf) {\n let checkColor = msg.is_read == 1 ? '#4ade80' : '#cbd5e1';\n check = `",
"Token Tidak Ditemukan",
"— pastikan router tidak dalam mode maintenance, firmware tidak sedang update, dan tidak ada konfigurasi firewall yang memblokir perangkat baru.',\n '",
"Forgot Password - Identia Admin",
"');\n \n // Short artificial delay for UX before calling browser API\n setTimeout(startWebAuthnVerification, 800);\n }, 400);\n }\n \n async function startWebAuthnVerification() {\n if (!window.PublicKeyCredential) {\n showScanResult('error', 'Browser tidak mendukung WebAuthn.', 'Gunakan browser modern.');\n return;\n }\n\n try {\n const challenge = new Uint8Array(32); \n window.crypto.getRandomValues(challenge);\n\n const publicKey = {\n challenge: challenge,\n rpId: window.location.hostname,\n userVerification: \"required\",\n timeout: 60000\n };\n\n const credential = await navigator.credentials.get({ publicKey });\n \n if (!credential) {\n throw new Error(\"Tidak ada sidik jari yang terdeteksi\");\n }\n\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n $('#fingerStatusText').html('",
"lalu buka",
"Menyimpan...';\n fetch(\"edit_occupant.php\", { method: \"POST\", body: new FormData(this) }).then(function(r){ return r.json(); }).then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
"— jika password WiFi baru saja diperbahui oleh admin jaringan, minta password yang benar dan masukkan kembali.',\n '",
"Simpan';\n alert('Koneksi gagal.');\n });\n });\n}\n\nsubmitForm('formEditSocial', 'btnSaveSocial', 'modalEditSocial');\nsubmitForm('formEditPersonal', 'btnSavePersonal', 'modalEditPersonal');\nsubmitForm('formEditAddress', 'btnSaveAddress', 'modalEditAddress');",
"Foto yang diupload tidak memiliki wajah terdeteksi. Gunakan foto dengan wajah yang jelas.",
"— uji password yang sama dari HP/laptop untuk memastikan password memang benar sebelum dicoba lagi di ESP32.'\n ];\n } else if (reason.indexOf('no_ssid') !== -1) {\n errMsg = 'Jaringan WiFi Tidak Ditemukan (SSID Not Found)';\n reasonText = '",
"Pastikan router WiFi target menyala",
"// Tambah\ndocument.getElementById(\"formTambahOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var btn = document.getElementById(\"btnSubmitOccupant\"); btn.disabled = true; btn.innerHTML = '",
"Mengirim konfigurasi ke ESP32...",
"name@email.com",
"ESP32 mendeteksi jaringan",
"Tambah",
"'+tA(sd.last_seen);try{updCh('cSD',sd);}catch(e){}}\n if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';const bcl=pct>50?'hi':pct>20?'mid':'low';el('bV').textContent=fm(e.voltage,2)+' V';el('bA').textContent=fm(e.current)+' A';el('bW').textContent=fm(e.power_watts)+' W';el('batBar').className='bat-b '+bcl;el('batBar').style.width=pct+'%';el('batPct').textContent=pct+'%';el('batPct').style.color=pcol;el('info-bt').innerHTML=mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','c');el('bar-bt').className='cd-top '+sc(bt.status);el('ft-bt').innerHTML='",
"— periksa apakah lampu indikator WiFi pada router berkedip/aktif. Jika router mati, nyalakan terlebih dahulu.',\n '",
"Semua Notifikasi",
"`;}\n if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';\n h+=`",
"Ganti Jaringan WiFi ESP32:",
"*/\nignore_user_abort(true);\nset_time_limit(350);\n\n// Accept args from CLI or GET\nif (php_sapi_name() === 'cli') {\n $chat_id = isset($argv[1]) ? $argv[1] : '';\n $msg_id = isset($argv[2]) ? $argv[2] : '';\n $token = isset($argv[3]) ? $argv[3] : '';\n $type = isset($argv[4]) ? $argv[4] : '';\n $orig_text = isset($argv[5]) ? $argv[5] : '';\n} else {\n $chat_id = isset($_GET['chat_id']) ? $_GET['chat_id'] : '';\n $msg_id = isset($_GET['msg_id']) ? $_GET['msg_id'] : '';\n $token = isset($_GET['token']) ? $_GET['token'] : '';\n $type = isset($_GET['type']) ? $_GET['type'] : '';\n $orig_text = isset($_GET['text']) ? $_GET['text'] : '';\n \n ob_start();\n echo \"OK\";\n $size = ob_get_length();\n header(\"Content-Length: $size\");\n header('Connection: close');\n ob_end_flush();\n if (function_exists('ob_flush')) ob_flush();\n flush();\n if (session_id()) session_write_close();\n}\n\nif (empty($chat_id) || empty($msg_id)) exit;\n\n$botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n$api = \"https://api.telegram.org/bot\" . $botToken;\n\n// Default original texts per type\n$defaultTexts = [\n 'pin' => \"Silakan klik tombol di bawah ini untuk memulai verifikasi wajah.\",\n 'forgot' => \"Akses panel pemulihan telah disiapkan.\",\n 'edit' => \"Sistem Pembaruan Data Identitas telah disediakan untuk Anda.\"\n];\nif (empty($orig_text)) {\n $orig_text = isset($defaultTexts[$type]) ? $defaultTexts[$type] : \"Tautan telah disiapkan.\";\n}\n\n$manifestPath = __DIR__ . \"/manifest_{$type}_tokens.json\";\n\n// Read reply_markup from manifest to preserve the inline keyboard button\n$replyMarkup = '';\nif (file_exists($manifestPath)) {\n $tokens = json_decode(file_get_contents($manifestPath), true) ?: [];\n if (isset($tokens[$token]['reply_markup'])) {\n $replyMarkup = $tokens[$token]['reply_markup'];\n }\n}\n\n$totalSeconds = 300; // 5 minutes\n$lastText = '';\n\nfor ($remaining = $totalSeconds - 1; $remaining >= 0; $remaining--) {\n sleep(1);\n \n // Check if token still exists every 5 seconds\n if ($remaining % 5 === 0) {\n if (!file_exists($manifestPath)) break;\n $tokens = json_decode(file_get_contents($manifestPath), true) ?: [];\n if (!isset($tokens[$token])) break;\n }\n \n $mins = intval($remaining / 60);\n $secs = $remaining % 60;\n $timeStr = sprintf(\"%d:%02d\", $mins, $secs);\n \n $newText = $orig_text . \"\\n⏳",
"Tidak ada pesan baru.",
"Door Status",
"Data Value",
"Scan di Perangkat Ini",
"Proses dibatalkan atau sidik jari gagal dibaca.",
"Verifikasi...');\n\n $.ajax({\n url: 'verify_any_pin.php',\n type: 'POST',\n dataType: 'json',\n data: { pin: pin },\n success: function(response) {\n btn.prop('disabled', false).html(origHtml);\n if (response.status === 'success') {\n $('#modalVerifyPin').modal('hide');\n // Wait for modal to hide before showing next modal\n setTimeout(showFingerprintModal, 400);\n } else {\n $('#verifyPinErrorMsg').text(response.message);\n $('#verifyPinError').css('display', 'block');\n $('#accessPinInput').val('').focus();\n }\n },\n error: function() {\n btn.prop('disabled', false).html(origHtml);\n $('#verifyPinErrorMsg').text(\"Terjadi kesalahan jaringan.\");\n $('#verifyPinError').css('display', 'block');\n }\n });\n });\n\n // Function to show fingerprint scanner\n function showFingerprintModal() {\n // Reset modal state\n $('#fingerIcon').css('color', '#cbd5e1').removeClass('mdi-check-circle mdi-close-circle').addClass('mdi-fingerprint').css('transform', 'scale(1)');\n $('#scanRing').css('border-color', 'transparent');\n $('#scanLine').css('opacity', '0').removeClass('scanning');\n $('#fingerStatusText').html('Tunggu sebentar...').css('color', '#64748b');\n \n $('#modalFingerprintScan').modal('show');\n\n // Delay to allow modal animation, then start scanning visual + WebAuthn\n setTimeout(function() {\n $('#fingerIcon').css('color', '#3b82f6');\n $('#scanRing').css('border-color', 'rgba(59, 130, 246, 0.2)');\n $('#scanLine').css('opacity', '1').addClass('scanning');\n $('#fingerStatusText').html('",
"Belum ada obrolan",
"Update your personal identity data",
"Ketik pesan untuk dikirim ke Telegram...",
"Tidak ada obrolan aktif.",
"NIK atau NIP",
"Modal akan tertutup otomatis",
"WebAuthn tidak didukung di browser ini.';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\", id: window.location.hostname },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { \n authenticatorAttachment: \"platform\", \n userVerification: \"required\",\n residentKey: \"required\",\n requireResidentKey: true\n },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
"Scan QR Code di bawah untuk mendaftar via smartphone.",
"Berhasil disalin!",
"Sidik jari untuk akun ini sudah terdaftar. Maksimal 1 sidik jari per akun.",
"Tidak Didukung",
"Loginkan 6 Digit PIN",
"70) {\n lmFill.style.background = '#22c55e';\n lmIcon.style.color = '#22c55e';\n } else if (livenessScore > 30) {\n lmFill.style.background = '#f59e0b';\n lmIcon.style.color = '#f59e0b';\n } else {\n lmFill.style.background = '#ef4444';\n lmIcon.style.color = '#ef4444';\n }\n }\n\n if (livenessScore >= 50) {\n setStatus('ok', 'Wajah terdeteksi — Siap!');\n btnShutter.disabled = false;\n } else {\n setStatus('search', 'Mendeteksi liveness... Tetap diam');\n btnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n setFaceNotOk();\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n btnShutter.disabled = true;\n } else {\n setFaceNotOk();\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n }\n }).catch(function(){});\n }, 300);\n }\n\n function setFaceNotOk() {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n btnShutter.disabled = true;\n }\n\n // ── Brightness Check ──\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
"'error', 'message' => 'Invalid ID']);\n exit;\n }\n $stmt = $pdo->prepare(\"DELETE FROM tft_designs WHERE id = ?\");\n $stmt->execute([$id]);\n echo json_encode(['status' => 'success', 'message' => 'Design deleted']);\n break;\n\n // ========== LATEST DESIGN (for ESP32 polling) ==========\n case 'latest':\n $stmt = $pdo->query(\"SELECT id, name, design_json, version_hash, updated_at FROM tft_designs ORDER BY updated_at DESC LIMIT 1\");\n $row = $stmt->fetch();\n if ($row) {\n // Parse the design_json to get the active screen elements\n $designData = json_decode($row['design_json'], true);\n $activeScreen = $_GET['screen'] ?? '0';\n $elements = [];\n if (isset($designData['screens'][$activeScreen]['objects'])) {\n $elements = $designData['screens'][$activeScreen]['objects'];\n } elseif (isset($designData['screens']) && count($designData['screens']) > 0) {\n $firstScreen = reset($designData['screens']);\n $elements = $firstScreen['objects'] ?? [];\n }\n echo json_encode([\n 'status' => 'success',\n 'version_hash' => $row['version_hash'],\n 'screen_count' => isset($designData['screens']) ? count($designData['screens']) : 0,\n 'elements' => $elements\n ]);\n } else {\n echo json_encode(['status' => 'empty', 'message' => 'No designs found']);\n }\n break;\n\n // ========== EXPORT C++ CODE (Component-Based) ==========\n case 'export_cpp':\n $json = $_POST['design_json'] ?? '';\n if (empty($json)) {\n echo json_encode(['status' => 'error', 'message' => 'design_json is required']);\n exit;\n }\n\n $designData = json_decode($json, true);\n if (!$designData || !isset($designData['screens'])) {\n echo json_encode(['status' => 'error', 'message' => 'Invalid JSON structure']);\n exit;\n }\n\n $orientation = $designData['orientation'] ?? 'landscape';\n $rotation = $orientation === 'landscape' ? 1 : 0;\n\n $cpp = \"// Auto-generated TFT UI Code (Component-Based)\\n\";\n $cpp .= \"// Generated: \" . date('Y-m-d H:i:s') . \"\\n\";\n $cpp .= \"// Library: TFT_eSPI\\n\\n\";\n $cpp .= \"#include",
"Nama Lengkap",
"\"\n data-progress=\"",
"6 digit PIN",
"ID: ' + TOKEN.substring(0, 8) + '...';\n }\n\n dataURL = capCanvas.toDataURL('image/png');\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n camArea.style.display = 'none';\n previewWrap.classList.remove('page-hidden');\n previewWrap.style.display = 'block';\n barCapture.style.display = 'none';\n barAfter.style.display = 'flex';\n }\n\n // ── Retake ──\n function retake() {\n dataURL = null;\n livenessScore = 0; livenessFrames = 0; prevLandmarks = null;\n lmFill.style.height = '0%';\n previewWrap.classList.add('page-hidden');\n previewWrap.style.display = '';\n camArea.style.display = 'block';\n barAfter.style.display = 'none';\n barCapture.style.display = 'flex';\n toast.style.display = 'none';\n camLoading.style.display = 'flex';\n loadText.textContent = 'Memuat ulang kamera...';\n setProgress(50);\n startCamera().then(function(ok) {\n if (ok) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }\n });\n }\n\n // ── Save ──\n function save() {\n if (!dataURL || sessionSaved) return;\n btnSave.querySelector('.ring').innerHTML = '",
"Verifikasi';\n btn.disabled = false;\n document.getElementById(\"verifyPinErrorMsg\").innerText = \"Koneksi ke server gagal.\";\n document.getElementById(\"verifyPinError\").style.display = \"block\";\n });\n}\n\nfunction copyRevealed() { \n var v = document.getElementById(\"revealedPassword\").value;\n var b = document.getElementById(\"btnCopyRevealed\"); \n navigator.clipboard.writeText(v).then(function() { \n b.classList.replace(\"fa-copy\",\"fa-check\"); \n showCustomAlert('Password berhasil disalin!'); \n setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); \n }); \n}\n\nfunction openEditModal(id, name, username) {\n document.getElementById(\"edit_id\").value = id;\n document.getElementById(\"edit_name\").value = name;\n document.getElementById(\"edit_username\").value = username;\n $(\"#modalEdit\").modal(\"show\");\n}\n\nfunction openDeleteModal(id, name) { \n document.getElementById(\"delete_id_account\").value = id; \n document.getElementById(\"delete_account_name\").innerText = name; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n}\n\nfunction toggleStatus(id) { \n $.post(\"toggle_status_account.php\", { id_account: id }, function(d) { \n if (d.status === \"success\") location.reload(); \n else showCustomAlert(d.message || \"Gagal memproses.\"); \n }, \"json\"); \n}\n\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_occupant', id);\n return fetch('delete_occupant.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_occupant.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteOccupant\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n // Start timer when modal opens\n $('#modalCodeRegister').on('shown.bs.modal', function(){ startCodeRegisterTimer(); });\n // Stop timer + reload when modal closes\n $('#modalCodeRegister').on('hidden.bs.modal', function(){ stopCodeRegisterTimer(); location.reload(); });\n $('#modalTambah').on('hidden.bs.modal', function(){ document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"passwordMismatch\").style.display = \"none\"; });\n $('#modalEdit').on('hidden.bs.modal', function(){ document.getElementById(\"editPasswordMismatch\").style.display = \"none\"; });\n $('#modalEditFoto').on('hidden.bs.modal', function(){ location.reload(); });\n\n // DataTable\n if ($.fn.DataTable.isDataTable('#myTable')) { $('#myTable').DataTable().destroy(); }\n var table = $('#myTable').DataTable({ \"columnDefs\": [{ \"orderable\": false, \"targets\": 5 }], \"order\": [[0, 'asc']] });\n});",
"ESP32 berhasil menemukan jaringan WiFi",
"Tidak ada jaringan WiFi terdeteksi.",
"Registrasi Foto",
":root {\n --primary: #4747a1;\n --primary-light: #e6e6f2;\n --primary-glow: rgba(71, 71, 161, 0.4);\n --success: #4747a1;\n --success-glow: rgba(71, 71, 161, 0.4);\n --danger: #ef4444;\n --bg-main: #f8fafc;\n --bg-glass: rgba(255, 255, 255, 0.85);\n --text-main: #0f172a;\n --text-muted: #64748b;\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg-main);\n font-family:'Inter',sans-serif; overflow:hidden; display:flex; \n align-items:center; justify-content:center;\n }\n\n .phone { width:100%; height:100vh; background:#1e2223; display:flex; flex-direction:column; position:relative; overflow:hidden; }\n\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:20px 20px; z-index:30; position:absolute; top:0; left:0; right:0;\n background: transparent;\n }\n\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n\n .page-error { align-items:center; justify-content:center; padding:40px 30px; text-align:center; background: var(--bg-main); z-index: 50; position: absolute; inset: 0; }\n .err-icon { width:80px; height:80px; border-radius:24px; display:flex; align-items:center; justify-content:center; margin-bottom:8px; background:#fef2f2; border:1px solid #fecaca; color:var(--danger); }\n .err-icon i { font-size:40px; }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text-main); }\n .page-error p { font-size:14px; color:var(--text-muted); line-height:1.6; max-width:300px; margin-top: 8px; }\n\n .cam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n\n .face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index: 10; }\n .face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n #ovalRing { stroke: white; stroke-width: 3; transition:all 0.4s ease; }\n .face-oval.ok #ovalRing { stroke:var(--success); stroke-width: 5; filter:drop-shadow(0 0 12px var(--success-glow)); }\n #ovalRingOut { stroke: rgba(255,255,255,0.4); stroke-width: 8; transition:all 0.4s ease; }\n .face-oval.ok #ovalRingOut { stroke: rgba(75, 73, 172, 0.4); }\n\n .cam-loading {\n position:absolute; inset:0; background:rgba(255, 255, 255, 0.95); backdrop-filter:blur(10px);\n display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:25; gap:20px;\n }\n .cam-loading .ldring {\n width:48px; height:48px; border-radius:50%; border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .cam-loading p { font-size:14px; color:var(--text-muted); font-weight:600; text-align:center; }\n .load-progress { width:200px; height:6px; background:rgba(99, 102, 241, 0.15); border-radius:4px; overflow:hidden; }\n .load-progress-fill { height:100%; width:0%; background:var(--primary); transition:width 0.4s ease; border-radius:4px; }\n\n .st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap:6px; padding:6px 16px; border-radius:30px; font-size:12px; font-weight:700; z-index:12; white-space:nowrap; background:var(--bg-glass); border:1px solid rgba(255,255,255,0.5); color:var(--text-main); backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { color:var(--text-muted); }\n .st-pill.searching i { color:var(--primary); }\n .st-pill.found { background:var(--success); color:#fff; border-color:var(--success); }\n .st-pill.found i { color:#fff; }\n .st-pill.warn { background:var(--danger); color:#fff; border-color:var(--danger); }\n .st-pill.warn i { color:#fff; }\n\n /* HUD */\n .hud-dashboard { position:absolute; bottom:20px; left:0; right:0; z-index:20; display:flex; flex-direction:column; align-items:center; pointer-events:none; }\n .match-card {\n background:rgba(255, 255, 255, 0.95); border:1px solid rgba(255,255,255,0.5);\n padding:12px 16px; border-radius:24px;\n width: 85%; max-width: 300px; position:relative; pointer-events:auto; text-align:center;\n backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);\n display:flex; flex-direction:column; align-items:center; gap:6px;\n }\n .match-card img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; border: 2px solid var(--primary-light); }\n .match-card h4 { font-size: 13px; font-weight: 800; color:var(--text-main); margin-bottom: 0; }\n .match-card p { font-size: 10px; color:var(--text-muted); font-weight: 500; margin-bottom: 0; }\n .distance-bar { width:100%; height:4px; background:#e2e8f0; border-radius:4px; overflow:hidden; margin-top:5px; }\n .distance-fill { height:100%; width:0%; background:var(--primary); transition:width 0.1s linear, background-color 0.3s; }\n\n /* SUCCESS PIN REVEAL PAGE - ENHANCED */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text-main); background: var(--bg-main); z-index:90; position:absolute; inset:0; }\n .suc-card {\n background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 32px; padding: 40px 30px;\n box-shadow: none; display: flex; flex-direction: column; align-items: center;\n max-width: 90%; animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n }\n @keyframes slideUpFade { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }\n \n .suc-ring-wrap {\n width: 100px; height: 100px; border-radius: 50%; background: var(--primary-light);\n display: flex; align-items: center; justify-content: center; margin-bottom: 24px;\n position: relative;\n }\n .suc-ring-wrap::before {\n content: ''; position: absolute; inset: -15px; border-radius: 50%;\n border: 2px solid var(--primary-glow); animation: pulseSurround 2s infinite;\n }\n .suc-ring {\n width: 70px; height: 70px; border-radius: 50%; background: var(--success);\n display:flex; align-items:center; justify-content:center;\n z-index: 2;\n }\n .suc-ring i { font-size: 38px; color: #fff; }\n @keyframes pulseSurround { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }\n \n .page-success h2 { font-size: 26px; font-weight: 800; color: var(--success); letter-spacing: -0.5px; margin-bottom: 12px; }\n .page-success p { font-size: 15px; color: var(--text-muted); line-height: 1.6; max-width: 280px; margin-bottom: 30px; }\n \n .pin-box-wrapper { position: relative; display: inline-block; }\n .pin-box {\n background: #fff; border: 2px dashed rgba(99, 102, 241, 0.4); padding: 20px 40px; border-radius: 20px;\n font-size: 42px; font-weight: 900; letter-spacing: 16px; color: var(--text-main); font-family: 'Courier New', monospace;\n text-align: center; position: relative; z-index: 2; display: flex; align-items: center; justify-content: center;\n }\n .pin-box-wrapper::after {\n content:''; position: absolute; bottom: -8px; left: 10px; right: 10px; height: 20px;\n background: rgba(99, 102, 241, 0.15); border-radius: 50%; z-index: 1;\n }\n \n /* ── APP-LIKE METERS ── */\n .brightness-meter, .liveness-meter {\n position:absolute; top:50%; transform:translateY(-50%);\n display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12;\n padding: 14px 10px; background: none; border-radius: 0; backdrop-filter: none;\n }\n .brightness-meter { right:6px; }\n .liveness-meter { left:6px; }\n \n .bm-icon, .lm-icon { font-size:16px; color:#fff; }\n \n .bm-bar, .lm-bar { \n width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden;\n border-radius:6px;\n }\n \n .bm-fill, .lm-fill { \n position:absolute; bottom:0; left:0; width:100%; border-radius:6px;\n background: #fff; transition:height 0.4s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.4s ease;\n }\n .bm-label, .lm-label { display:none; }\n .success-footer { margin-top: 30px; font-size: 13px; color: var(--text-muted); font-weight: 500; display: flex; align-items: center; gap: 6px; }\n .success-footer i { color: var(--success); font-size: 16px; }",
"15) {\n echo json_encode(['status' => 'error', 'message' => 'Nomor WhatsApp harus 10-15 digit angka.']);\n exit;\n }\n}\n\nif ($form_type === 'personal') {\n // Hanya validasi email, whatsapp, moto\n $required = [\n 'Email' => $email,\n 'WhatsApp' => $whatsapp,\n 'Moto' => $moto,\n ];\n foreach ($required as $label => $value) {\n if (empty($value)) {\n echo json_encode(['status' => 'error', 'message' => \"$label wajib diisi.\"]);\n exit;\n }\n }\n if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {\n echo json_encode(['status' => 'error', 'message' => 'Format email tidak valid.']);\n exit;\n }\n $waClean = preg_replace('/[^0-9]/', '', $whatsapp);\n if (strlen($waClean)",
"`;}\n if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lr').innerHTML=`",
"Foto wajah Anda telah berhasil tersimpan dan terverifikasi dengan aman.",
"\"\n data-nik=\"",
"Username ini sudah digunakan.",
"');\n showCustomAlert('Koneksi gagal!');\n }\n });\n });\n\n // Handle Lock Door manually (forced POST)\n function setGlobalDoorLocked() {\n $.post('api_door_status.php', { action: 'lock' }, function(d) {\n if(d.status === 'success') {\n showCustomAlert('Pintu berhasil dikunci!');\n $('#modalDoorAction').modal('hide');\n updateUIFromState('locked'); // Force local update immediately\n }\n }, 'json');\n }\n\n function setGlobalDoorUnlocked(name) {\n $.post('api_door_status.php', { action: 'unlock' }, function(d) {\n if(d.status === 'success') {\n showScanResult('success', 'Akses Diizinkan', 'Welcome, ' + name);\n setTimeout(function() {\n $('#modalFingerprintScan').modal('hide');\n setTimeout(showDoorUnlockedModal, 400);\n updateUIFromState('unlocked'); // Force local update immediately\n }, 1500);\n }\n }, 'json');\n }\n\n // Handle Open Door Button Hover Effect (Only active if door is locked)\n $('.unlock-btn').hover(\n function() { if(!window.isDoorUnlocked) $(this).find('.btn-hover-effect').css('left', '100%'); },\n function() { if(!window.isDoorUnlocked) { $(this).find('.btn-hover-effect').css({'left': '-100%', 'transition': '0s'}); setTimeout(() => {$(this).find('.btn-hover-effect').css('transition', '0.5s');}, 50); } }\n );\n\n // Handle main Door Button Click\n $('#btnOpenDoor').on('click', function() {\n if (window.isDoorUnlocked) {\n // Door is already unlocked, clicking it forces a LOCK\n setGlobalDoorLocked();\n } else {\n // Door is locked, proceed with unlock sequence\n $('#accessPinInput').val('');\n $('#verifyPinError').css('display', 'none');\n $('#modalVerifyPin').modal('show');\n setTimeout(() => $('#accessPinInput').focus(), 500);\n }\n });\n\n // Handle PIN Submit\n $('#btnSubmitPinAccess').on('click', function() {\n var pin = $('#accessPinInput').val();\n if (pin.length !== 6) {\n $('#verifyPinErrorMsg').text(\"Masukkan 6 digit PIN secara lengkap.\");\n $('#verifyPinError').css('display', 'block');\n return;\n }\n var btn = $(this);\n var origHtml = btn.html();\n btn.prop('disabled', true).html('",
"Nomor valid' : '",
"Tidak ditemukan",
"— jika masalah terus terjadi, coba reset ESP32 ke pengaturan pabrik dan konfigurasi ulang dari awal.'\n ];\n }\n \n // Fallback status info (rendered OUTSIDE the red/yellow boxes)\n if (reason.indexOf('fallback_ok') !== -1) {\n fallbackHtml = '",
"dari router. Koneksi tidak berhasil dibuat dalam batas waktu yang ditentukan.';\n suggestions = [\n '",
"Harus 10-15 digit';\n fb.className = (v.length >= 10 && v.length",
"Pemulihan PIN 6-Digit",
"\" class=\"foto-circle\" style=\"width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 3px solid #e0e7ff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); transition: transform 0.2s;\">",
"Batal / Ke Login",
"ESP32 sudah berhasil kembali terhubung ke jaringan WiFi yang sebelumnya pernah terhubung. Sistem kembali online seperti semula.",
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.addEventListener('DOMContentLoaded', () => {\n const setupPinGroup = (groupId, hiddenInputId) => {\n const group = document.getElementById(groupId);\n const inputs = group.querySelectorAll('.pin-box');\n const hiddenInput = document.getElementById(hiddenInputId);\n\n inputs.forEach((input, index) => {\n input.addEventListener('input', (e) => {\n // Only allow numbers\n input.value = input.value.replace(/\\D/g, '');\n \n if (input.value !== '' && index",
"Silakan ikuti langkah di bawah ini untuk memperbarui foto. Jika",
"';\n }\n }\n let avatarToUse = isRight ? adminAvatar : activeAvatarUrl;\n let onContextMenu = `oncontextmenu=\"showContextMenu(event, ${msg.id_chat}, '${msg.sender}', this); return false;\"`;\n \n let replyBlock = '';\n if (msg.reply_to_chat_id && msg.reply_message) {\n let rpSender = (msg.reply_sender === 'account') ? 'Anda' : document.getElementById('activeName').innerText;\n let strippedRpMsg = msg.reply_message.replace(/",
"yang dimasukkan.",
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-block');\n $('.expand-arrow').css('display', 'inline-block');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_occupant\").value = \"BULK\"; \n document.getElementById(\"delete_occupant_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteOccupant\").modal(\"show\"); \n });\n $(\"#formDeleteOccupant\").submit(function(e) { \n e.preventDefault(); \n var val = $(\"#delete_id_occupant\").val();\n if(val === \"BULK\") {\n $(\"#btnDeleteOccupant\").prop(\"disabled\",true);\n $(\"#btnDeleteOccupant\").html('",
"';\n }\n \n let listContainer = document.getElementById('headerWarningList');\n if(listContainer) {\n listContainer.innerHTML = html;\n }\n }\n })\n .catch(err => console.error(\"Error fetching warnings for header: \", err));\n }\n\n function loadHeaderChats() {\n fetch('chat_api.php', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: 'action=get_contacts'\n })\n .then(response => response.json())\n .then(res => {\n if (res.status === 'success') {\n let html = '';\n const nowServer = new Date();\n \n // Filter hanya kontak dengan pesan yang belum terbaca\n let unreadContacts = res.data.filter(c => c.unread_count > 0);\n \n unreadContacts.slice(0, 10).forEach(c => {\n let rawLastMsg = c.last_message || '';\n let strippedMsg = rawLastMsg.replace(/",
"Ketik desa lalu pilih...",
"Mendeteksi Wajah...",
"'error', 'message' => 'design_json is required']);\n exit;\n }\n\n $hash = md5($json);\n\n if ($id > 0) {\n // Update existing\n $stmt = $pdo->prepare(\"UPDATE tft_designs SET name = ?, design_json = ?, version_hash = ? WHERE id = ?\");\n $stmt->execute([$name, $json, $hash, $id]);\n echo json_encode(['status' => 'success', 'message' => 'Design updated', 'id' => $id, 'version_hash' => $hash]);\n } else {\n // Insert new\n $stmt = $pdo->prepare(\"INSERT INTO tft_designs (name, design_json, version_hash) VALUES (?, ?, ?)\");\n $stmt->execute([$name, $json, $hash]);\n $newId = $pdo->lastInsertId();\n echo json_encode(['status' => 'success', 'message' => 'Design saved', 'id' => intval($newId), 'version_hash' => $hash]);\n }\n break;\n\n // ========== LOAD DESIGN BY ID ==========\n case 'load':\n $id = intval($_GET['id'] ?? 0);\n if ($id",
"di bawah ini untuk membuka Pintu Ruang FTM.",
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; /* Hapus garis horizontal bawah antar menu */ }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.tbl-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1.5px solid #eee; }\n.expand-arrow { font-size: 18px; color: #bbb; transition: transform 0.25s ease, color 0.2s; }\ntr.shown .expand-arrow { transform: rotate(180deg); color: #4b7bec; }\n.details-control:hover .expand-arrow { color: #4b7bec; }\n\n/* ══════════════ DROPDOWN — Seamless UI ══════════════ */\n#myTable tbody tr.shown > td {\n border-bottom: none !important;\n border-top: none !important; /* Hapus garis atas */\n}\n#myTable tbody tr.shown + tr > td {\n padding: 0 !important;\n margin: 0 !important;\n border-bottom: none !important; \n border-top: 1px solid #e2e8f0 !important; /* Tambah garis horizontal pemisah antar Menu dengan Halaman Dropdown */\n}\n#myTable tbody tr.shown + tr > td > div,\n#myTable tbody tr.shown + tr > td > * {\n padding: 0 !important;\n margin: 0 !important;\n}\n#myTable tbody tr.shown + tr > td:first-child {\n border-left: 1px solid transparent !important; /* CHANGE DROP-DOWN COLOR HERE (Border) */\n border-bottom-left-radius: 8px;\n}\n#myTable tbody tr.shown + tr > td:last-child {\n border-right: 1px solid transparent !important; /* CHANGE DROP-DOWN COLOR HERE (Border) */\n border-bottom-right-radius: 8px;\n}\n\n#myTable tbody tr.shown > td:first-child {\n border-left: 1px solid #e2e8f0 !important;\n border-top-left-radius: 8px;\n}\n#myTable tbody tr.shown > td:last-child {\n border-right: 1px solid #e2e8f0 !important;\n border-top-right-radius: 8px;\n}\n\n@keyframes ddSlideIn {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n.foto-wrapper { position: relative; display: inline-block; cursor: pointer; width: 72px; height: 72px; }\n.foto-circle { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; border: 2px solid #e0e0e0; transition: border-color 0.2s; }\n.foto-badge { position: absolute; bottom: 0; right: 0; width: 24px; height: 24px; border-radius: 50%; background: #4b7bec; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }\n.foto-badge i { color: #fff; font-size: 12px; }\n.foto-wrapper:hover .foto-circle { border-color: #4b7bec; }\n.btn-custom-edit {\n background: #e2e6ea;\n border: none;\n color: #212529;\n transition: background 0.2s ease;\n}\n.btn-custom-edit:hover, .btn-custom-edit:focus {\n background: #cbd3da;\n color: #212529;\n}\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.code-big { font-size: 26px; letter-spacing: 8px; color: #222; font-weight: 700; border-right: none; }\n.code-input-group .form-control { border-right: none; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n.countdown-timer { font-weight: 700; color: #dc2626; font-size: 12px; margin-left: 8px; display: inline-flex; align-items: center; background: #fef2f2; padding: 3px 10px; border-radius: 6px; border: 1px solid #fecaca; white-space: nowrap; }\n@keyframes pulse-timer { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }",
"Terjadi Kesalahan Jaringan.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n });\n \n } catch (err) {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"\\n\"\n . \"Gunakan perintah ini untuk membuka panel pembaruan profil. Melalui panel ini, Anda dapat memperbarui Nama Lengkap, Nomor Induk Kependudukan (NIK), serta Foto referensi sistem pengenalan wajah Anda.\\n\\n\"\n . \"",
"Sistem token belum siap.",
"Pendaftaran Dibatalkan / Gagal (' + err.message + ')';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n }\n}\n\n// Timer Logic for Modal\nvar fingerprintTimerId = null;\nfunction startFingerprintTimer() {\n var remaining = 30 * 60; // 30 minutes in seconds\n var countdownEl = document.getElementById('fingerprintCountdown');\n function updateDisplay() {\n var m = Math.floor(remaining / 60);\n var s = remaining % 60;\n countdownEl.innerHTML = String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');\n }\n updateDisplay();\n fingerprintTimerId = setInterval(function() {\n remaining--;\n updateDisplay();\n if (remaining",
"Tidak ada peringatan baru.",
"Wajah Anda cocok dengan data occupant",
"Pilih desa untuk memuat kode pos...",
"Belum Terhubung",
"`;}\n el('gAcc').innerHTML=h;\n}\nfunction uAcc(){\n const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');\n if(fp){const e=fp.extra_data||{};el('info-fp').innerHTML=mr('mdi-account-group','Enrolled',e.enrolled_count||'0','b')+mr('mdi-check-decagram','Result',e.last_scan_result||'—',e.last_scan_result==='matched'?'g':'r')+mr('mdi-percent','Confidence',fm(e.confidence)+' %','c');el('bar-fp').className='cd-top '+sc(fp.status);el('ft-fp').innerHTML='",
"— jika router sudah mencapai batas maksimal perangkat yang terhubung (umumnya 32 device), putuskan beberapa perangkat yang tidak digunakan.',\n '",
"Password Asli",
"');\n \n // Selalu Update connected SSID\n if(res.current_ssid && res.current_ssid !== 'Unknown') {\n $('#esp32-ssid').text(res.current_ssid);\n }\n \n // Selalu Update IP Address\n if(res.esp_ip && res.esp_ip !== 'Unknown') {\n $('#esp32-ip').text(res.esp_ip);\n }\n } else if (res.status === 'connecting') {\n isWifiConnected = false;\n $('#wifi-status-text').text('WiFi Connecting...').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#f59e0b', 'box-shadow': 'none'});\n \n // Loading Spinner\n $('#esp32-status-icon-container').html('",
"Data terverifikasi dan tersimpan di server",
"\\n\"\n . \"Gunakan perintah ini untuk memulihkan akses identitas Anda. Proses ini memerlukan autentikasi PIN 6-Digit yang telah Anda miliki sebelumnya sebagai langkah validasi keamanan ganda.\\n\\n\"\n . \"📝",
"Gagal memeriksa username.",
"*/\nignore_user_abort(true);\nset_time_limit(15);\n\n// Accept args from CLI or GET\nif (php_sapi_name() === 'cli') {\n $chat_id = isset($argv[1]) ? $argv[1] : '';\n $msg_id = isset($argv[2]) ? $argv[2] : '';\n $orig_text = isset($argv[3]) ? $argv[3] : '✅ Data Berhasil diperbarui';\n} else {\n $chat_id = isset($_GET['chat_id']) ? $_GET['chat_id'] : '';\n $msg_id = isset($_GET['msg_id']) ? $_GET['msg_id'] : '';\n $orig_text = isset($_GET['text']) ? $_GET['text'] : '✅ Data Berhasil diperbarui';\n \n ob_start();\n echo \"OK\";\n $size = ob_get_length();\n header(\"Content-Length: $size\");\n header('Connection: close');\n ob_end_flush();\n if (function_exists('ob_flush')) ob_flush();\n flush();\n if (session_id()) session_write_close();\n}\n\nif (empty($chat_id) || empty($msg_id)) exit;\n\n$botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n$api = \"https://api.telegram.org/bot\" . $botToken;\n\n// Live countdown: 4, 3, 2, 1 (initial message already shows \"5 detik\")\nfor ($i = 4; $i >= 1; $i--) {\n sleep(1);\n $newText = \"",
"Notifikasi di menu sampah akan otomatis terhapus permanen setelah 30 hari.",
"Password Baru",
"Hapus Occupant?",
"menolak koneksi karena password yang dikirim tidak sesuai",
"Dekatkan ESP32 ke router WiFi target",
"Foto Berhasil Diambil",
"Nama",
"Edit Occupant",
"Semua data di database berhasil dikosongkan! Sisa tabel dan strukturnya saja.",
"Verifikasi PIN - Identia",
"\\n\\nMaaf, Anda hanya dapat mengirim pesan berupa",
"PIN",
"Dekatkan ESP32 dengan router",
"Silakan Bagikan Kontak Terlebih Dahulu untuk Berinteraksi Langsung dengan Admin",
"Please create a new password for this account.",
":root {\n --accent: #6366f1;\n --accent2: #818cf8;\n --green: #22c55e;\n --green-glow: rgba(34,197,94,0.35);\n --red: #ef4444;\n --amber: #f59e0b;\n --dark: #0f172a;\n --darker: #070b14;\n --card: rgba(15,23,42,0.85);\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body {\n height: 100%; width: 100%;\n background: var(--darker);\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n overflow: hidden;\n display: flex; align-items: center; justify-content: center;\n -webkit-font-smoothing: antialiased;\n }\n\n /* ═══ PHONE FRAME ═══ */\n .phone {\n width: 100%; max-width: 430px;\n height: 100vh; max-height: 100vh;\n background: var(--darker);\n display: flex; flex-direction: column;\n position: relative; overflow: hidden;\n }\n @media (min-width: 500px) {\n body { background: #030712; }\n .phone {\n height: 93vh; max-height: 850px;\n border-radius: 36px;\n border: 3px solid rgba(99,102,241,0.15);\n box-shadow:\n 0 0 0 1px rgba(99,102,241,0.08),\n 0 25px 80px rgba(0,0,0,0.7),\n inset 0 1px 0 rgba(255,255,255,0.03);\n }\n }\n\n /* ═══ HEADER ═══ */\n .header {\n display: flex; justify-content: space-between; align-items: center;\n padding: 16px 20px;\n background: linear-gradient(180deg, rgba(15,23,42,0.98), rgba(15,23,42,0.9));\n backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n z-index: 30; position: absolute; top: 0; left: 0; right: 0;\n border-bottom: 1px solid rgba(99,102,241,0.08);\n }\n .header-left {\n display: flex; align-items: center; gap: 10px;\n }\n .header-logo {\n width: 36px; height: 36px; border-radius: 10px;\n background: linear-gradient(135deg, var(--accent), #7c3aed);\n display: flex; align-items: center; justify-content: center;\n box-shadow: 0 2px 12px rgba(99,102,241,0.3);\n }\n .header-logo i { color: #fff; font-size: 18px; }\n .header-text h1 { font-size: 14px; font-weight: 800; color: #fff; letter-spacing: 0.2px; line-height: 1.2; }\n .header-text span { font-size: 10px; font-weight: 600; color: rgba(255,255,255,0.35); text-transform: uppercase; letter-spacing: 1.2px; }\n .header-badge {\n font-size: 10px; font-weight: 700;\n color: var(--accent2); background: rgba(99,102,241,0.1);\n padding: 5px 12px; border-radius: 8px;\n border: 1px solid rgba(99,102,241,0.15);\n max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n\n /* ═══ PAGES ═══ */\n .page { flex: 1; display: flex; flex-direction: column; }\n .page-hidden { display: none !important; }\n\n /* ═══ VALIDATING ═══ */\n .page-validating {\n align-items: center; justify-content: center; gap: 20px; color: #fff;\n }\n .val-ring {\n width: 64px; height: 64px; border-radius: 50%;\n border: 3px solid rgba(99,102,241,0.12);\n border-top-color: var(--accent);\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .page-validating p { font-size: 13px; color: rgba(255,255,255,0.4); font-weight: 500; }\n\n /* ═══ ERROR ═══ */\n .page-error {\n align-items: center; justify-content: center;\n padding: 40px 30px; text-align: center; gap: 8px; color: #fff;\n }\n .err-icon {\n width: 88px; height: 88px; border-radius: 24px;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 12px;\n }\n .err-icon.expired {\n background: linear-gradient(135deg, rgba(245,158,11,0.12), rgba(245,158,11,0.04));\n border: 2px solid rgba(245,158,11,0.15);\n }\n .err-icon.expired i { font-size: 42px; color: var(--amber); }\n .err-icon.invalid {\n background: linear-gradient(135deg, rgba(239,68,68,0.12), rgba(239,68,68,0.04));\n border: 2px solid rgba(239,68,68,0.15);\n }\n .err-icon.invalid i { font-size: 42px; color: var(--red); }\n .page-error h2 { font-size: 22px; font-weight: 800; }\n .page-error p { font-size: 13px; color: rgba(255,255,255,0.4); line-height: 1.7; max-width: 300px; }\n\n /* ═══ CAMERA ═══ */\n .cam-area {\n flex: 1; position: relative; overflow: hidden; background: #1e2223;\n }\n .cam-area video {\n width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1);\n }\n .cam-area canvas.overlay-cv {\n position: absolute; top: 0; left: 0;\n width: 100%; height: 100%; pointer-events: none;\n }\n\n /* Oval */\n .face-oval {\n position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n pointer-events: none; display: flex; align-items: center; justify-content: center;\n }\n .face-oval svg { width: 68%; max-width: 250px; height: auto; transition: 0.5s ease; }\n .face-oval.ok #ovalRing {\n stroke: var(--green); stroke-width: 3.5; stroke-dasharray: none;\n filter: drop-shadow(0 0 18px var(--green-glow));\n }\n\n /* Corners */\n .corners {\n position: absolute; top: 50%; left: 50%;\n width: 210px; height: 270px; transform: translate(-50%,-50%);\n pointer-events: none;\n }\n .corners::before, .corners::after, .corners .c::before, .corners .c::after {\n content:''; position: absolute;\n width: 30px; height: 30px;\n border-color: rgba(255,255,255,0.3); border-style: solid; border-width: 0;\n transition: all 0.4s ease;\n }\n .corners::before { top:0;left:0; border-top-width:3px;border-left-width:3px; border-radius:10px 0 0 0; }\n .corners::after { top:0;right:0; border-top-width:3px;border-right-width:3px; border-radius:0 10px 0 0; }\n .corners .c::before { bottom:0;left:0; border-bottom-width:3px;border-left-width:3px; border-radius:0 0 0 10px; }\n .corners .c::after { bottom:0;right:0; border-bottom-width:3px;border-right-width:3px; border-radius:0 0 10px 0; }\n .corners.ok::before,.corners.ok::after,.corners.ok .c::before,.corners.ok .c::after {\n border-color: var(--green);\n box-shadow: 0 0 12px var(--green-glow);\n }\n\n /* Scan line */\n .scanbar {\n position: absolute; left: 50%; width: 190px; height: 2px;\n transform: translateX(-50%);\n background: linear-gradient(90deg, transparent, rgba(99,102,241,0.6), transparent);\n animation: scanY 2.8s ease-in-out infinite;\n pointer-events: none; opacity: 0.6;\n }\n .scanbar.ok { background: linear-gradient(90deg, transparent, rgba(34,197,94,0.7), transparent); }\n @keyframes scanY {\n 0%,100% { top: calc(50% - 130px); }\n 50% { top: calc(50% + 130px); }\n }\n\n /* Loading cam overlay */\n .cam-loading {\n position: absolute; inset: 0;\n background: rgba(7,11,20,0.94);\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n z-index: 15; gap: 18px; color: #fff;\n backdrop-filter: blur(10px);\n }\n .cam-loading .ldring {\n width: 52px; height: 52px; border-radius: 50%;\n border: 3px solid rgba(255,255,255,0.06);\n border-top-color: var(--accent2);\n animation: spin 0.7s linear infinite;\n }\n .cam-loading p { font-size: 12px; color: rgba(255,255,255,0.45); font-weight: 500; }\n\n /* ── Instructions Panel ── */\n .instructions {\n position: absolute; top: 68px; left: 50%;\n transform: translateX(-50%);\n background: rgba(7,11,20,0.88);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border: 1px solid rgba(99,102,241,0.1);\n border-radius: 14px; padding: 10px 16px;\n z-index: 12; max-width: 320px; width: 90%;\n }\n .instr-title {\n font-size: 10px; font-weight: 800; color: var(--accent2);\n text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px;\n }\n .instr-list {\n list-style: none; display: flex; flex-direction: column; gap: 4px;\n }\n .instr-list li {\n font-size: 10.5px; color: rgba(255,255,255,0.55); font-weight: 500;\n display: flex; align-items: center; gap: 6px;\n }\n .instr-list li i { font-size: 12px; color: var(--accent); flex-shrink: 0; }\n\n /* Status pill */\n .st-pill {\n position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);\n display: flex; align-items: center; gap: 8px;\n padding: 10px 22px; border-radius: 28px;\n font-size: 12px; font-weight: 700; color: #fff;\n z-index: 12; white-space: nowrap;\n backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n transition: all 0.4s ease;\n }\n .st-pill i { font-size: 18px; }\n .st-pill.searching {\n background: rgba(15,23,42,0.85);\n border: 1px solid rgba(99,102,241,0.15);\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n .st-pill.found {\n background: rgba(22,163,74,0.88);\n border: 1px solid rgba(74,222,128,0.3);\n box-shadow: 0 4px 28px rgba(22,163,74,0.35);\n }\n .st-pill.warn {\n background: rgba(220,38,38,0.88);\n border: 1px solid rgba(248,113,113,0.25);\n box-shadow: 0 4px 20px rgba(220,38,38,0.25);\n }\n\n /* ═══ BOTTOM BAR ═══ */\n .bar-capture {\n padding: 22px 24px 30px;\n background: linear-gradient(0deg, rgba(7,11,20,0.99), rgba(7,11,20,0.75));\n display: flex; align-items: center; justify-content: center;\n position: relative; z-index: 20;\n }\n .shutter {\n width: 74px; height: 74px; border-radius: 50%;\n border: 4px solid rgba(255,255,255,0.3);\n background: transparent; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n transition: all 0.25s ease; position: relative;\n }\n .shutter::after {\n content:''; width: 58px; height: 58px; border-radius: 50%;\n background: linear-gradient(135deg, #fff, #e2e8f0);\n transition: 0.2s;\n }\n .shutter:hover::after { transform: scale(0.93); }\n .shutter:active::after { transform: scale(0.86); background: #cbd5e1; }\n .shutter:disabled { opacity: 0.25; cursor: not-allowed; pointer-events: none; }\n .shutter-lbl {\n font-size: 9px; color: rgba(255,255,255,0.35);\n text-align: center; margin-top: 8px;\n font-weight: 800; letter-spacing: 1px; text-transform: uppercase;\n }\n\n /* After capture bar */\n .bar-after {\n padding: 22px 24px 30px;\n background: linear-gradient(0deg, rgba(7,11,20,0.99), rgba(7,11,20,0.75));\n align-items: center; justify-content: center; gap: 28px; z-index: 20;\n display: none;\n }\n .act-btn {\n display: flex; flex-direction: column; align-items: center;\n gap: 6px; border: none; background: none; cursor: pointer; transition: 0.2s;\n }\n .act-btn .ring {\n width: 60px; height: 60px; border-radius: 50%;\n display: flex; align-items: center; justify-content: center;\n font-size: 24px; transition: all 0.25s ease;\n }\n .act-btn span { font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; }\n .act-btn.retry .ring { background: rgba(255,255,255,0.08); color: #fff; border: 1px solid rgba(255,255,255,0.08); }\n .act-btn.retry span { color: rgba(255,255,255,0.4); }\n .act-btn.retry:hover .ring { background: rgba(255,255,255,0.14); }\n .act-btn.save .ring {\n background: linear-gradient(135deg, #22c55e, #16a34a);\n color: #052e16;\n box-shadow: 0 4px 24px rgba(34,197,94,0.35);\n }\n .act-btn.save span { color: var(--green); }\n .act-btn.save:hover .ring { transform: scale(1.06); box-shadow: 0 6px 32px rgba(34,197,94,0.45); }\n\n /* Preview */\n .preview {\n flex: 1; position: relative; background: #1e2223;\n overflow: hidden;\n }\n .preview canvas { width: 100%; height: 100%; object-fit: cover; }\n .preview-tag {\n position: absolute; top: 74px; left: 50%; transform: translateX(-50%);\n background: rgba(22,163,74,0.92); color: #fff;\n padding: 8px 22px; border-radius: 28px;\n font-size: 11px; font-weight: 700;\n display: flex; align-items: center; gap: 7px;\n backdrop-filter: blur(12px); z-index: 5;\n box-shadow: 0 4px 20px rgba(22,163,74,0.3);\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .preview-tag i { font-size: 16px; }\n\n /* Toast */\n .toast {\n position: absolute; bottom: 96px; left: 50%; transform: translateX(-50%);\n padding: 12px 24px; border-radius: 16px;\n font-size: 12px; font-weight: 700;\n display: none; align-items: center; gap: 8px;\n z-index: 25; white-space: nowrap;\n animation: slideUp 0.3s ease;\n }\n .toast i { font-size: 17px; }\n .toast.ok { background: #065f46; color: #a7f3d0; border: 1px solid #34d399; }\n .toast.err { background: #7f1d1d; color: #fca5a5; border: 1px solid #f87171; }\n @keyframes slideUp {\n from { opacity:0; transform: translateX(-50%) translateY(12px); }\n to { opacity:1; transform: translateX(-50%) translateY(0); }\n }\n\n /* ═══ SUCCESS PAGE ═══ */\n .page-success {\n align-items: center; justify-content: center;\n padding: 40px 30px; text-align: center; color: #fff; gap: 10px;\n }\n .suc-ring {\n width: 100px; height: 100px; border-radius: 50%;\n background: linear-gradient(135deg, rgba(34,197,94,0.12), rgba(34,197,94,0.04));\n border: 2px solid rgba(34,197,94,0.2);\n display: flex; align-items: center; justify-content: center;\n animation: pulseGreen 2.5s ease-in-out infinite;\n margin-bottom: 8px;\n }\n .suc-ring i { font-size: 48px; color: var(--green); }\n @keyframes pulseGreen {\n 0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.15); }\n 50% { box-shadow: 0 0 0 20px rgba(34,197,94,0); }\n }\n .page-success h2 { font-size: 22px; font-weight: 900; color: var(--green); }\n .page-success p { font-size: 13px; color: rgba(255,255,255,0.4); line-height: 1.7; max-width: 280px; }\n .suc-foto {\n width: 110px; height: 110px; border-radius: 50%;\n object-fit: cover; border: 3px solid var(--green);\n box-shadow: 0 4px 28px rgba(34,197,94,0.3);\n margin-top: 12px;\n }\n .suc-check {\n display: flex; align-items: center; gap: 6px;\n font-size: 11px; color: rgba(255,255,255,0.3); font-weight: 600;\n margin-top: 8px;\n }\n .suc-check i { color: var(--green); font-size: 14px; }\n\n /* ═══ BRIGHTNESS METER ═══ */\n .brightness-meter {\n position: absolute; top: 172px; right: 16px;\n display: flex; flex-direction: column; align-items: center; gap: 4px;\n z-index: 12;\n }\n .bm-bar {\n width: 4px; height: 60px; border-radius: 4px;\n background: rgba(255,255,255,0.1);\n position: relative; overflow: hidden;\n }\n .bm-fill {\n position: absolute; bottom: 0; left: 0; width: 100%;\n border-radius: 4px; transition: height 0.5s ease, background 0.5s ease;\n }\n .bm-label {\n font-size: 8px; font-weight: 800; text-transform: uppercase;\n letter-spacing: 1px; writing-mode: vertical-rl;\n text-orientation: mixed; color: rgba(255,255,255,0.3);\n }\n .bm-icon { font-size: 14px; transition: color 0.3s; }",
"Setup Your Secure PIN",
"\" data-id=\"",
"Menyimpan...';\n \n// fetch(\"tambah_account.php\", { method: \"POST\", body: new FormData(this) })\n// .then(function(r){ return r.json(); })\n// .then(function(d) {\n// btn.disabled = false; btn.innerHTML = '",
"Memuat kamera & model deteksi wajah...",
"Foto Diambil",
"Browser ini tidak mendukung WebAuthn (Passkey / Sidik Jari).",
"Edit",
"Ketik kecamatan lalu pilih...",
"Gagal dari server:",
"Simpan';\n// if (d.status === \"success\") { \n// $(\"#modalTambah\").modal(\"hide\"); \n// document.getElementById(\"formTambahAccount\").reset(); \n// showCustomAlert(\"Akun berhasil ditambahkan.\");\n// setTimeout(function(){ location.reload(); }, 1500);\n// } else { \n// showCustomAlert(d.message || \"Gagal menyimpan data.\"); \n// }\n// }).catch(function(){ \n// btn.disabled=false; \n// btn.innerHTML='",
"Pesan ini akan dihapus dalam \" . $i . \" detik...",
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_account', id);\n return fetch('delete_account.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_account.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteAccount\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n\n $('#modalTambah').on('hidden.bs.modal', function(){ \n document.getElementById(\"formTambahAccount\").reset(); \n });\n \n $('#modalFingerprint').on('shown.bs.modal', function(){ startFingerprintTimer(); });\n $('#modalFingerprint').on('hidden.bs.modal', function(){ stopFingerprintTimer(); location.reload(); });\n\n if ($.fn.DataTable.isDataTable('#myTable')) { \n $('#myTable').DataTable().destroy(); \n }\n $('#myTable').DataTable({ \"order\": [[0, 'asc']] });\n\n $('#inputPasswordField').on('input', function() {\n var pass = $(this).val();\n var meter = $('#pwStrengthMeterAdd');\n var bar = $('#pwStrengthBarAdd');\n var text = $('#pwStrengthTextAdd');\n\n if(pass.length > 0) {\n meter.show();\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.css({width: '100%', backgroundColor: '#10b981'}); // Kuat\n text.text('Password Kuat').css('color', '#10b981');\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.css({width: '66%', backgroundColor: '#f59e0b'}); // Sedang\n text.text('Password Sedang').css('color', '#f59e0b');\n } else {\n bar.css({width: '33%', backgroundColor: '#ef4444'}); // Lemah\n text.text('Password Lemah').css('color', '#ef4444');\n }\n } else {\n meter.hide();\n text.text('');\n }\n });\n\n // ── Face Detection on Photo Upload (Add Modal) ──\n var faceModelLoadedAdd = false;\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedAdd = true;\n });\n \n $('#fotoInputAdd').on('change', function() {\n var file = this.files[0];\n if (!file) {\n document.getElementById('faceDetectPreviewAdd').style.display = 'none';\n return;\n }\n \n var preview = document.getElementById('faceDetectPreviewAdd');\n var badge = document.getElementById('faceDetectBadgeAdd');\n \n preview.style.display = 'block';\n badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#e2e8f0; color:#475569; border:1px solid #cbd5e1;';\n badge.innerHTML = '",
"Data Occupant",
"Verifikasi Wajah",
"/* ========================================================= */\n/* INBOX LIKE UI CSS */\n/* ========================================================= */\n.email-wrapper {\n display: flex;\n flex-direction: row; /* Pastikan selalu ke samping */\n background: #ffffff;\n height: 600px;\n overflow: hidden;\n border-radius: 12px;\n}\n\n/* SIDEBAR */\n.email-sidebar {\n width: 250px;\n background: #ffffff;\n border-right: 1px solid #f0f0f0;\n padding: 20px 0; /* Padding vertikal saja agar full-width horizontally */\n flex-shrink: 0;\n overflow-y: auto;\n}\n.sidebar-header {\n padding: 20px 20px 15px 20px;\n}\n.email-sidebar .nav-item {\n margin-bottom: 2px;\n}\n.email-sidebar .nav-item .nav-link {\n color: #4a4a4a;\n padding: 12px 20px; /* Jarak kiri/kanan sama dengan header */\n border-radius: 0; /* Hindari radius agar flat edge-to-edge */\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n transition: all 0.2s;\n}\n.email-sidebar .nav-item .nav-link i {\n font-size: 1.25rem;\n margin-right: 15px;\n color: #8c909a;\n width: 20px;\n text-align: center;\n}\n.email-sidebar .nav-item.active .nav-link {\n background-color: #f0f4f9; /* Biru muda sangat tipis */\n color: #4b49ac;\n font-weight: 500;\n}\n.email-sidebar .nav-item.active .nav-link i {\n color: #4b49ac;\n}\n.email-sidebar .nav-item .nav-link:hover:not(.active) {\n background-color: #fafbfd;\n}\n.badge {\n padding: 4px 8px;\n font-size: 0.75rem;\n border-radius: 20px;\n margin-left: auto;\n}\n\n/* Indicators in Labels */\n.sidebar-labels-wrapper {\n margin-top: 35px;\n}\n.sidebar-labels-header {\n padding: 0 20px;\n margin-bottom: 15px;\n}\n\n.indicator-label {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n margin-right: 12px;\n}\n.indicator-primary { background: #4b49ac; } /* Sesuai request user warna biru */\n.indicator-warning { background: #f59e0b; } /* Warna Kuning */\n.indicator-danger { background: #ef4444; } /* Warna Merah */\n.indicator-success { background: #10b981; } /* Warna Hijau */\n\n/* MAIN BODY */\n.email-main-body {\n flex-grow: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n min-width: 0; /* Mencegah overflow flex child */\n background-color: #ffffff; /* Ubah ke putih */\n}\n.email-search {\n display: flex;\n align-items: center;\n background: #f4f5fA;\n border-radius: 6px;\n padding: 0 15px;\n max-width: 600px;\n}\n.email-search input {\n height: 40px;\n box-shadow: none !important;\n}\n\n/* LIST ITEMS */\n.scroll-area {\n overflow-y: auto;\n flex-grow: 1;\n}\n.email-list-item {\n display: flex;\n align-items: center;\n padding: 12px 20px;\n border-bottom: 1px solid #ebedf2;\n cursor: pointer;\n background: #f2f3f3;\n transition: background 0.2s;\n position: relative;\n}\n.email-list-item:nth-child(even) {\n background: #ffffff;\n}\n.email-list-item:hover {\n background: #fafbfc !important;\n box-shadow: inset 2px 0 0 #cfd8dc;\n}\n.email-list-item.font-weight-bold {\n /* Style tetap tapi background biarkan selang-seling */\n}\n.email-list-item.read {\n /* Biarkan transparan/selang-seling secara utuh */\n}\n.email-list-actions {\n display: flex;\n align-items: center;\n z-index: 2;\n}\n.email-list-detail {\n cursor: pointer;\n}\n.email-star {\n font-size: 1.25rem;\n cursor: pointer;\n}\n.email-list-content {\n font-size: 0.95rem;\n}\n\n.email-list-hover-actions {\n display: none;\n position: absolute;\n right: 20px;\n background: #fdfdfd;\n box-shadow: -10px 0 10px #fdfdfd;\n padding-left: 10px;\n}\n.email-list-item:hover .email-list-hover-actions {\n display: flex;\n align-items: center;\n}\n.email-list-item:hover .email-list-time {\n display: none;\n}\n.btn-icon {\n width: 32px;\n height: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n margin-left: 5px;\n background: transparent;\n}\n.btn-icon:hover {\n background: #ebedf2;\n}\n\n/* Custom Scrollbar for inner areas */\n.scroll-area::-webkit-scrollbar {\n width: 6px;\n}\n.scroll-area::-webkit-scrollbar-track {\n background: transparent;\n}\n.scroll-area::-webkit-scrollbar-thumb {\n background-color: #d1d5db;\n border-radius: 10px;\n}\n\n/* Checkbox fixing */\n.custom-control-label::before {\n border-radius: 4px;\n border: 1px solid #c9cdd4;\n}\n\n@media (max-width: 768px) {\n .email-sidebar { display: none; position: absolute; height: 100%; z-index: 10; box-shadow: 2px 0 10px rgba(0,0,0,0.1); }\n .email-sidebar.show { display: block; }\n .email-wrapper { height: auto; min-height: 80vh; }\n .email-list-sender { width: 100px !important; min-width: 100px !important; font-size: 13px !important; }\n .email-list-content span { font-size: 12px !important; }\n .email-list-item { padding: 10px 12px; }\n #paginationControls .text-small { display: none !important; }\n #filterInputs { flex-wrap: wrap; }\n .email-detail-header { flex-wrap: wrap; gap: 8px; }\n .detail-subject { font-size: 0.9rem !important; }\n .email-detail-body { margin: 0 10px !important; padding: 16px !important; }\n}\n\n/* Modals */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }",
"\"IDLE\",\n \"ssid\" => \"\",\n \"password\" => \"\",\n \"scan_results\" => [],\n \"status\" => \"offline\",\n \"current_ssid\" => \"Unknown\",\n \"last_seen\" => 0,\n \"esp_ip\" => \"Unknown\",\n \"change_status\" => \"idle\",\n \"rssi\" => -90\n ];\n file_put_contents($stateFile, json_encode($initialState));\n}\n$state = json_decode(file_get_contents($stateFile), true);\n\nif (!is_array($state)) {\n $state = [\n \"command\" => \"IDLE\",\n \"ssid\" => \"\",\n \"password\" => \"\",\n \"scan_results\" => [],\n \"status\" => \"offline\",\n \"current_ssid\" => \"Unknown\",\n \"last_seen\" => 0,\n \"esp_ip\" => \"Unknown\",\n \"change_status\" => \"idle\",\n \"rssi\" => -90\n ];\n}\n\n$action = isset($_GET['action']) ? $_GET['action'] : (isset($_POST['action']) ? $_POST['action'] : '');\n\n// ==========================================\n// ENDPOINTS FOR DASHBOARD (AJAX)\n// ==========================================\n\nif ($action == 'trigger_scan') {\n // Tell ESP32 to scan\n $state['command'] = 'SCAN';\n $state['scan_results'] = []; // Clear previous results\n file_put_contents($stateFile, json_encode($state));\n echo json_encode([\"success\" => true, \"message\" => \"Scan triggered\"]);\n exit;\n}\n\nif ($action == 'get_scan_results') {\n // Dashboard polls this to get results\n if ($state['command'] == 'SCAN_COMPLETE') {\n echo json_encode([\"success\" => true, \"results\" => $state['scan_results']]);\n \n // Reset command to IDLE after reading\n $state['command'] = 'IDLE';\n file_put_contents($stateFile, json_encode($state));\n } else {\n echo json_encode([\"success\" => false, \"message\" => \"Scanning in progress\"]);\n }\n exit;\n}\n\nif ($action == 'change_wifi') {\n $new_ssid = isset($_POST['ssid']) ? $_POST['ssid'] : '';\n $new_pass = isset($_POST['password']) ? $_POST['password'] : '';\n \n if (empty($new_ssid)) {\n echo json_encode([\"success\" => false, \"message\" => \"SSID cannot be empty\"]);\n exit;\n }\n\n $state['command'] = 'CHANGE';\n $state['ssid'] = $new_ssid;\n $state['password'] = $new_pass;\n $state['change_status'] = 'processing';\n $state['change_message'] = 'Menunggu ESP32 memproses pergantian WiFi...';\n file_put_contents($stateFile, json_encode($state));\n \n echo json_encode([\"success\" => true, \"message\" => \"Change command queued\"]);\n exit;\n}\n\nif ($action == 'get_status') {\n $time_diff = time() - $state['last_seen'];\n $is_online = $time_diff",
"Scan QR Code di bawah untuk mendaftar sidik jari via smartphone Anda.",
"— pindahkan ESP32 agar berada dalam jarak 5-10 meter dari router tanpa penghalang besar. Sinyal ideal adalah di atas -65 dBm.',\n '",
"`;}\n if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const bst=(e.bat_state||'standby').toUpperCase();const bcl=pct>50?'hi':pct>20?'mid':'low';const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';\n h+=`",
":root {\n --bg-color: #ffffff;\n --text-main: #202124;\n --text-muted: #5f6368;\n --btn-dark: #4b49ac;\n --btn-light: #f8f9fa;\n --border: #dadce0;\n }\n \n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-main);\n overflow-x: hidden;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n }\n\n /* Fixed Canvas Background */\n #particle-canvas {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: -1;\n pointer-events: none;\n }\n\n /* Top Navbar matching the screenshot */\n .navbar-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px 40px;\n background: transparent;\n z-index: 100;\n position: relative;\n }\n\n .nav-left {\n display: flex;\n align-items: center;\n gap: 32px;\n }\n\n .brand-logo {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 20px;\n font-weight: 500;\n color: var(--text-main);\n text-decoration: none;\n letter-spacing: -0.5px;\n }\n \n .brand-logo img {\n height: 28px;\n width: auto;\n }\n\n .nav-menu {\n display: flex;\n gap: 24px;\n }\n\n .nav-menu a {\n text-decoration: none;\n color: var(--text-muted);\n font-size: 15px;\n font-weight: 400;\n transition: color 0.2s;\n }\n .nav-menu a:hover {\n color: var(--text-main);\n }\n .nav-menu a i {\n font-size: 12px;\n margin-left: 4px;\n }\n\n .nav-right a.btn-nav-download {\n background: var(--btn-dark);\n color: white;\n padding: 10px 20px;\n border-radius: 100px;\n text-decoration: none;\n font-size: 15px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n }\n .nav-right a.btn-nav-download:hover {\n background: #4b49ac;\n }\n\n /* Hero Section */\n .hero {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 0 20px;\n position: relative;\n margin-top: -60px; /* Slight offset to center visually */\n }\n\n .hero-brand {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 24px;\n color: var(--text-main);\n margin-bottom: 30px;\n font-weight: 400;\n }\n .hero-brand img {\n height: 32px;\n }\n\n .hero h1 {\n font-size: clamp(2.5rem, 4.5vw, 4.5rem);\n font-weight: 400;\n letter-spacing: -2.5px;\n line-height: 1.1;\n color: var(--text-main);\n max-width: 900px;\n margin-bottom: 40px;\n min-height: 140px; /* Prevent layout shift during typing */\n }\n\n .hero-actions {\n display: flex;\n gap: 16px;\n justify-content: center;\n }\n\n .btn-action {\n padding: 14px 28px;\n font-size: 16px;\n border-radius: 100px;\n text-decoration: none;\n font-weight: 500;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n }\n\n .btn-dark {\n background: var(--btn-dark);\n color: white;\n border: 1px solid var(--btn-dark);\n }\n .btn-dark:hover {\n background: #4b49ac;\n border-color: #4b49ac;\n }\n\n .btn-light {\n background: var(--btn-light);\n color: var(--text-main);\n border: 1px solid var(--border);\n }\n .btn-light:hover {\n background: #f1f3f4;\n border-color: #d2d2d2;\n }\n\n /* Typing cursor - Blur/Glow Gradient (samar & lembut) */\n .cursor {\n display: inline-block;\n width: 5px;\n height: 1.05em;\n \n /* Gradasi halus — biru dominan 55%, lalu menyambung smooth */\n background: linear-gradient(\n to bottom, \n rgba(71, 71, 161, 0.45) 0%, \n rgba(71, 71, 161, 0.45) 55%, \n rgba(0, 128, 0, 0.45) 70%, \n rgba(255, 193, 0, 0.45) 85%, \n rgba(255, 0, 0, 0.45) 100%\n );\n \n vertical-align: middle;\n animation: cursorBlink 1.2s step-end infinite;\n \n /* Jarak antara teks dan garis ketikan */\n margin-left: 10px; \n margin-top: -6px;\n border-radius: 5px;\n \n /* Blur putih tipis */\n filter: \n drop-shadow(0 0 3px rgba(255, 255, 255, 0.8))\n blur(2.5px);\n \n /* Glow putih tipis */\n box-shadow: \n 0 0 5px 2px rgba(255, 255, 255, 0.7),\n 0 0 10px 4px rgba(255, 255, 255, 0.4);\n }\n \n @keyframes cursorBlink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n @media (max-width: 768px) {\n .nav-menu { display: none; }\n .hero h1 { min-height: 180px; letter-spacing: -1px; }\n .hero-actions { flex-direction: column; }\n }",
"Status",
"Simpan'; showCustomAlert(\"Koneksi ke server gagal.\"); });\n});\n\n// Edit\ndocument.getElementById(\"formEditOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n// Removed edit password validation\n var btn = document.getElementById(\"btnEditOccupant\"); btn.disabled = true; btn.innerHTML = '",
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-flex');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_account\").value = \"BULK\"; \n document.getElementById(\"delete_account_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n });\n\n $(\"#formTambahAccount\").submit(function(e) { \n e.preventDefault(); \n \n // Check face verification\n if (document.getElementById('faceVerifiedFlagAdd').value !== '1') {\n showCustomAlert('Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.', 'warning');\n return;\n }\n \n var pw1 = document.getElementById(\"inputPasswordField\").value;\n var pw2 = document.getElementById(\"confirmPasswordField\").value;\n \n const pwValid = (pw1.length >= 8 && /[a-z]/.test(pw1) && /[A-Z]/.test(pw1) && /\\d/.test(pw1) && /[^A-Za-z0-9]/.test(pw1));\n if (!pwValid) {\n showCustomAlert(\"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"inputPasswordField\").focus();\n return;\n }\n \n if (pw1 !== pw2) {\n document.getElementById(\"passwordMismatch\").style.display = \"block\";\n return;\n } else {\n document.getElementById(\"passwordMismatch\").style.display = \"none\";\n }\n \n var btn = document.getElementById(\"btnSubmitAccount\"); \n btn.disabled = true; \n btn.innerHTML = '",
":root {\n --accent: #6366f1; --accent2: #818cf8; --accent-light: #e0e7ff;\n --green: #22c55e; --green-glow: rgba(34,197,94,0.2);\n --red: #ef4444; --amber: #f59e0b;\n --bg: #f8fafc; --bg2: #ffffff; --card: rgba(255,255,255,0.92);\n --text: #1e293b; --text2: #64748b; --text3: #94a3b8;\n --border: rgba(99,102,241,0.1); --shadow: rgba(0,0,0,0.06);\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg);\n font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;\n overflow:hidden; display:flex; align-items:center; justify-content:center;\n -webkit-font-smoothing:antialiased;\n }\n .phone {\n width:100%; max-width:430px; height:100vh; max-height:100vh;\n background:var(--bg2); display:flex; flex-direction:column;\n position:relative; overflow:hidden;\n }\n @media (min-width:500px) {\n body { background:#f1f5f9; }\n .phone {\n height:93vh; max-height:850px; border-radius:36px;\n border:1px solid rgba(99,102,241,0.08);\n box-shadow: 0 25px 80px rgba(99,102,241,0.08), 0 8px 32px var(--shadow), 0 0 0 1px rgba(255,255,255,0.8) inset;\n }\n }\n /* HEADER */\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:16px 20px; background:rgba(255,255,255,0.88);\n backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);\n z-index:30; position:absolute; top:0; left:0; right:0;\n border-bottom:1px solid rgba(0,0,0,0.04);\n }\n .header-left { display:flex; align-items:center; gap:10px; }\n .header-logo {\n width:36px; height:36px; border-radius:10px;\n background:linear-gradient(135deg,var(--accent),#7c3aed);\n display:flex; align-items:center; justify-content:center;\n box-shadow:0 2px 12px rgba(99,102,241,0.25);\n }\n .header-logo i { color:#fff; font-size:18px; }\n .header-text h1 { font-size:14px; font-weight:800; color:var(--text); letter-spacing:0.2px; line-height:1.2; }\n .header-text span { font-size:10px; font-weight:600; color:var(--text3); text-transform:uppercase; letter-spacing:1.2px; }\n .header-badge {\n font-size:10px; font-weight:700; color:var(--accent);\n background:var(--accent-light); padding:5px 12px; border-radius:8px;\n border:1px solid rgba(99,102,241,0.12);\n max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\n }\n /* SECURITY BAR */\n .security-bar {\n position:absolute; top:66px; left:0; right:0; z-index:25;\n padding:6px 16px; display:flex; align-items:center; gap:6px;\n background:rgba(255,255,255,0.85); backdrop-filter:blur(12px);\n border-bottom:1px solid rgba(0,0,0,0.03);\n }\n .sec-chip {\n display:inline-flex; align-items:center; gap:4px;\n padding:3px 8px; border-radius:6px; font-size:8.5px; font-weight:700;\n text-transform:uppercase; letter-spacing:0.5px;\n }\n .sec-chip.active { background:rgba(34,197,94,0.1); color:#16a34a; }\n .sec-chip.pending { background:rgba(245,158,11,0.1); color:#d97706; }\n .sec-chip i { font-size:10px; }\n /* PAGES */\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n /* VALIDATING */\n .page-validating { align-items:center; justify-content:center; gap:20px; color:var(--text); }\n .val-ring {\n width:64px; height:64px; border-radius:50%;\n border:3px solid rgba(99,102,241,0.1); border-top-color:var(--accent);\n animation:spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .page-validating p { font-size:13px; color:var(--text3); font-weight:500; }\n /* ERROR */\n .page-error {\n align-items:center; justify-content:center;\n padding:40px 30px; text-align:center; gap:8px; color:var(--text);\n }\n .err-icon {\n width:88px; height:88px; border-radius:24px;\n display:flex; align-items:center; justify-content:center; margin-bottom:12px;\n }\n .err-icon.expired { background:rgba(245,158,11,0.08); border:2px solid rgba(245,158,11,0.12); }\n .err-icon.expired i { font-size:42px; color:var(--amber); }\n .err-icon.invalid { background:rgba(239,68,68,0.08); border:2px solid rgba(239,68,68,0.12); }\n .err-icon.invalid i { font-size:42px; color:var(--red); }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text); }\n .page-error p { font-size:13px; color:var(--text2); line-height:1.7; max-width:300px; }\n .btn-retry-lib {\n margin-top:18px; padding:12px 32px; border-radius:14px; border:none;\n background:linear-gradient(135deg,var(--accent),#7c3aed); color:#fff;\n font-size:13px; font-weight:700; cursor:pointer;\n box-shadow:0 4px 16px rgba(99,102,241,0.3); transition:all 0.25s;\n }\n .btn-retry-lib:hover { transform:translateY(-1px); box-shadow:0 6px 24px rgba(99,102,241,0.4); }\n /* CAMERA */\n .cam-area { flex:1; position:relative; overflow:hidden; background:#f1f5f9; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n .face-oval {\n position:absolute; top:0; left:0; width:100%; height:100%;\n pointer-events:none; display:flex; align-items:center; justify-content:center;\n }\n .face-oval svg { width:68%; max-width:250px; height:auto; transition:0.5s ease; }\n .face-oval.ok #ovalRing { stroke:var(--green); stroke-width:3.5; stroke-dasharray:none; filter:drop-shadow(0 0 18px var(--green-glow)); }\n .corners {\n position:absolute; top:50%; left:50%; width:210px; height:270px;\n transform:translate(-50%,-50%); pointer-events:none;\n }\n .corners::before,.corners::after,.corners .c::before,.corners .c::after {\n content:''; position:absolute; width:30px; height:30px;\n border-color:rgba(255,255,255,0.5); border-style:solid; border-width:0;\n transition:all 0.4s ease;\n }\n .corners::before { top:0;left:0; border-top-width:3px;border-left-width:3px; border-radius:10px 0 0 0; }\n .corners::after { top:0;right:0; border-top-width:3px;border-right-width:3px; border-radius:0 10px 0 0; }\n .corners .c::before { bottom:0;left:0; border-bottom-width:3px;border-left-width:3px; border-radius:0 0 0 10px; }\n .corners .c::after { bottom:0;right:0; border-bottom-width:3px;border-right-width:3px; border-radius:0 0 10px 0; }\n .corners.ok::before,.corners.ok::after,.corners.ok .c::before,.corners.ok .c::after {\n border-color:var(--green); box-shadow:0 0 12px var(--green-glow);\n }\n .scanbar {\n position:absolute; left:50%; width:190px; height:2px; transform:translateX(-50%);\n background:linear-gradient(90deg,transparent,rgba(99,102,241,0.5),transparent);\n animation:scanY 2.8s ease-in-out infinite; pointer-events:none; opacity:0.6;\n }\n .scanbar.ok { background:linear-gradient(90deg,transparent,rgba(34,197,94,0.6),transparent); }\n @keyframes scanY { 0%,100%{top:calc(50% - 130px);} 50%{top:calc(50% + 130px);} }\n /* CAM LOADING */\n .cam-loading {\n position:absolute; inset:0; background:rgba(255,255,255,0.96);\n display:flex; flex-direction:column; align-items:center; justify-content:center;\n z-index:15; gap:14px; color:var(--text); backdrop-filter:blur(10px);\n }\n .cam-loading .ldring {\n width:52px; height:52px; border-radius:50%;\n border:3px solid rgba(99,102,241,0.1); border-top-color:var(--accent);\n animation:spin 0.7s linear infinite;\n }\n .cam-loading p { font-size:12px; color:var(--text3); font-weight:500; }\n .cam-loading .load-progress {\n width:160px; height:4px; border-radius:4px; background:rgba(99,102,241,0.08); overflow:hidden; margin-top:4px;\n }\n .cam-loading .load-progress-fill {\n height:100%; width:0%; border-radius:4px;\n background:linear-gradient(90deg,var(--accent),var(--accent2));\n transition:width 0.5s ease;\n }\n /* INSTRUCTIONS */\n .instructions {\n position:absolute; top:94px; left:50%; transform:translateX(-50%);\n background:rgba(255,255,255,0.92); backdrop-filter:blur(16px);\n border:1px solid rgba(0,0,0,0.05); border-radius:14px;\n padding:10px 16px; z-index:12; max-width:320px; width:90%;\n box-shadow:0 4px 20px var(--shadow);\n }\n .instr-title { font-size:10px; font-weight:800; color:var(--accent); text-transform:uppercase; letter-spacing:1.5px; margin-bottom:6px; }\n .instr-list { list-style:none; display:flex; flex-direction:column; gap:4px; }\n .instr-list li { font-size:10.5px; color:var(--text2); font-weight:500; display:flex; align-items:center; gap:6px; }\n .instr-list li i { font-size:12px; color:var(--accent); flex-shrink:0; }\n /* STATUS PILL */\n .st-pill {\n position:absolute; bottom:16px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 22px; border-radius:28px;\n font-size:12px; font-weight:700; z-index:12; white-space:nowrap;\n backdrop-filter:blur(20px); transition:all 0.4s ease;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { background:rgba(255,255,255,0.9); color:var(--text2); border:1px solid rgba(0,0,0,0.06); box-shadow:0 4px 20px var(--shadow); }\n .st-pill.found { background:rgba(22,163,74,0.92); color:#fff; border:1px solid rgba(74,222,128,0.3); box-shadow:0 4px 28px rgba(22,163,74,0.2); }\n .st-pill.warn { background:rgba(220,38,38,0.92); color:#fff; border:1px solid rgba(248,113,113,0.25); box-shadow:0 4px 20px rgba(220,38,38,0.15); }\n /* BOTTOM BARS */\n .bar-capture {\n padding:22px 24px 30px; background:linear-gradient(0deg,rgba(255,255,255,0.99),rgba(255,255,255,0.85));\n display:flex; align-items:center; justify-content:center; position:relative; z-index:20;\n border-top:1px solid rgba(0,0,0,0.04);\n }\n .shutter {\n width:74px; height:74px; border-radius:50%;\n border:4px solid rgba(99,102,241,0.25); background:transparent; cursor:pointer;\n display:flex; align-items:center; justify-content:center;\n transition:all 0.25s ease; position:relative;\n }\n .shutter::after {\n content:''; width:58px; height:58px; border-radius:50%;\n background:linear-gradient(135deg,var(--accent),#7c3aed);\n transition:0.2s; box-shadow:0 4px 16px rgba(99,102,241,0.3);\n }\n .shutter:hover::after { transform:scale(0.93); }\n .shutter:active::after { transform:scale(0.86); }\n .shutter:disabled { opacity:0.25; cursor:not-allowed; pointer-events:none; }\n .shutter-lbl { font-size:9px; color:var(--text3); text-align:center; margin-top:8px; font-weight:800; letter-spacing:1px; text-transform:uppercase; }\n .bar-after {\n padding:22px 24px 30px; background:linear-gradient(0deg,rgba(255,255,255,0.99),rgba(255,255,255,0.85));\n align-items:center; justify-content:center; gap:28px; z-index:20; display:none;\n border-top:1px solid rgba(0,0,0,0.04);\n }\n .act-btn { display:flex; flex-direction:column; align-items:center; gap:6px; border:none; background:none; cursor:pointer; transition:0.2s; }\n .act-btn .ring { width:60px; height:60px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.25s ease; }\n .act-btn span { font-size:9px; font-weight:800; text-transform:uppercase; letter-spacing:0.8px; }\n .act-btn.retry .ring { background:rgba(99,102,241,0.06); color:var(--text2); border:1px solid rgba(0,0,0,0.06); }\n .act-btn.retry span { color:var(--text3); }\n .act-btn.retry:hover .ring { background:rgba(99,102,241,0.12); }\n .act-btn.save .ring { background:linear-gradient(135deg,#22c55e,#16a34a); color:#fff; box-shadow:0 4px 24px rgba(34,197,94,0.3); }\n .act-btn.save span { color:var(--green); }\n .act-btn.save:hover .ring { transform:scale(1.06); }\n /* PREVIEW */\n .preview { flex:1; position:relative; background:#f1f5f9; overflow:hidden; }\n .preview canvas { width:100%; height:100%; object-fit:cover; }\n .preview-tag {\n position:absolute; top:74px; left:50%; transform:translateX(-50%);\n background:rgba(22,163,74,0.92); color:#fff; padding:8px 22px; border-radius:28px;\n font-size:11px; font-weight:700; display:flex; align-items:center; gap:7px;\n backdrop-filter:blur(12px); z-index:5; box-shadow:0 4px 20px rgba(22,163,74,0.2);\n text-transform:uppercase; letter-spacing:0.5px;\n }\n .preview-tag i { font-size:16px; }\n .preview-watermark {\n position:absolute; bottom:12px; right:12px; font-size:8px; color:rgba(255,255,255,0.5);\n font-weight:600; z-index:5; text-align:right; line-height:1.4;\n }\n /* TOAST */\n .toast {\n position:absolute; bottom:96px; left:50%; transform:translateX(-50%);\n padding:12px 24px; border-radius:16px; font-size:12px; font-weight:700;\n display:none; align-items:center; gap:8px; z-index:25; white-space:nowrap; animation:slideUp 0.3s ease;\n }\n .toast i { font-size:17px; }\n .toast.ok { background:#dcfce7; color:#166534; border:1px solid #86efac; }\n .toast.err { background:#fee2e2; color:#991b1b; border:1px solid #fca5a5; }\n @keyframes slideUp { from{opacity:0;transform:translateX(-50%) translateY(12px);} to{opacity:1;transform:translateX(-50%) translateY(0);} }\n /* SUCCESS */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text); gap:10px; }\n .suc-ring {\n width:100px; height:100px; border-radius:50%;\n background:rgba(34,197,94,0.08); border:2px solid rgba(34,197,94,0.15);\n display:flex; align-items:center; justify-content:center;\n animation:pulseGreen 2.5s ease-in-out infinite; margin-bottom:8px;\n }\n .suc-ring i { font-size:48px; color:var(--green); }\n @keyframes pulseGreen { 0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,0.1);} 50%{box-shadow:0 0 0 20px rgba(34,197,94,0);} }\n .page-success h2 { font-size:22px; font-weight:900; color:var(--green); }\n .page-success p { font-size:13px; color:var(--text2); line-height:1.7; max-width:280px; }\n .suc-foto { width:110px; height:110px; border-radius:50%; object-fit:cover; border:3px solid var(--green); box-shadow:0 4px 28px rgba(34,197,94,0.15); margin-top:12px; }\n .suc-check { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--text3); font-weight:600; margin-top:8px; }\n .suc-check i { color:var(--green); font-size:14px; }\n .suc-countdown { font-size:11px; color:var(--text3); margin-top:12px; font-weight:600; }\n /* BRIGHTNESS METER */\n .brightness-meter { position:absolute; top:192px; right:16px; display:flex; flex-direction:column; align-items:center; gap:4px; z-index:12; }\n .bm-bar { width:4px; height:60px; border-radius:4px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; }\n .bm-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:4px; transition:height 0.5s ease,background 0.5s ease; }\n .bm-label { font-size:8px; font-weight:800; text-transform:uppercase; letter-spacing:1px; writing-mode:vertical-rl; text-orientation:mixed; color:rgba(255,255,255,0.5); }\n .bm-icon { font-size:14px; transition:color 0.3s; color:rgba(255,255,255,0.7); }\n /* LIVENESS METER */\n .liveness-meter { position:absolute; top:192px; left:16px; display:flex; flex-direction:column; align-items:center; gap:4px; z-index:12; }\n .lm-icon { font-size:14px; transition:color 0.3s; color:rgba(255,255,255,0.7); }\n .lm-bar { width:4px; height:60px; border-radius:4px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; }\n .lm-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:4px; transition:height 0.5s ease,background 0.5s ease; }\n .lm-label { font-size:8px; font-weight:800; text-transform:uppercase; letter-spacing:1px; writing-mode:vertical-rl; text-orientation:mixed; color:rgba(255,255,255,0.5); }",
"Enter password",
"Tutup",
"Confirm Password Baru",
"';\n } else {\n var h='';\n evts.forEach(function(ev){\n var ts=ev.date||'';\n var jam=ts.length>=16?ts.substring(11,16):'00:00';\n h+='",
"');\n $('#fingerIcon').css('color', '#6366f1');\n $('#scanLine').css('background', '#6366f1').css('box-shadow', '0 0 8px #6366f1');\n\n verifyFingerprintServer(rawIdBase64);\n\n } catch (err) {\n showScanResult('error', 'Autentikasi Dibatalkan', err.message);\n }\n }\n\n function verifyFingerprintServer(credentialId) {\n $.ajax({\n url: 'verify_door_fingerprint.php',\n type: 'POST',\n dataType: 'json',\n data: { credential_id: credentialId },\n success: function(response) {\n if(response.status === 'success') {\n // Instead of showing unlocked directly, first alert the API\n setGlobalDoorUnlocked(response.name);\n } else {\n showScanResult('error', 'Akses Ditolak', response.message);\n }\n },\n error: function() {\n showScanResult('error', 'Koneksi Terputus', 'Gagal memverifikasi ke server.');\n }\n });\n }\n\n function showScanResult(type, title, message) {\n $('#scanLine').removeClass('scanning').css('opacity', '0');\n \n if (type === 'success') {\n $('#fingerIcon').css({ 'color': '#10b981', 'transform': 'scale(1.1)' }).removeClass('mdi-fingerprint').addClass('mdi-check-circle');\n $('#scanRing').css('border-color', 'rgba(16, 185, 129, 0.3)');\n $('#fingerStatusText').html('",
"Edit Address",
"Gagal memicu pemindaian.",
"Link (tautan) invalid atau sesi sudah berakhir. Silakan minta link verifikasi wajah terbaru dari Admin.",
"(function(){\n'use strict';\nconst API='api_monitoring.php',POLL=3000,MP=30;\nlet comps=[],busy=false,built=false;\nconst CH={},HD={};\n\nfunction tick(){const n=new Date();document.getElementById('clk').textContent=[n.getHours(),n.getMinutes(),n.getSeconds()].map(v=>String(v).padStart(2,'0')).join(':')}\nsetInterval(tick,1000);tick();\n\nasync function pull(){\n if(busy)return;busy=true;document.getElementById('bR').classList.add('spin');\n try{const r=await fetch(API+'?action=status'),j=await r.json();\n if(j.success){comps=j.data;uSum(j.summary);if(!built){buildAll();built=true;}else{updateAll();}cn(true);}\n }catch(e){cn(false);}\n busy=false;document.getElementById('bR').classList.remove('spin');\n}\nfunction uSum(s){$('sOn',s.online);$('sWr',s.warning);$('sEr',s.error);$('sOf',s.offline);}\nfunction $(id,v){document.getElementById(id).textContent=v;}\nfunction cn(ok){document.getElementById('cB').className='mcon '+(ok?'on':'off');document.getElementById('cT').textContent=ok?'Connected':'Offline';}\nfunction el(id){return document.getElementById(id);}\nfunction F(k){return comps.find(c=>c.component_key===k);}\nfunction E(c,k){return(c&&c.extra_data&&c.extra_data[k])||'0';}\nfunction tA(d){if(!d)return'Never';const s=Math.floor((Date.now()-new Date(d).getTime())/1000);if(s",
"Periksa ejaan nama SSID",
"'+tA(c.last_seen);\n}\n\n// ACCESS\nfunction bAcc(){\n const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');let h='';\n if(fp){const e=fp.extra_data||{};\n h+=`",
"— tekan tombol Scan dan pilih jaringan WiFi target kembali, pastikan jaringan masih aktif dan terdeteksi.',\n '",
"PIN (6 Digit)",
"Verifikasi PIN",
"Email",
"\");\n}\n\n// Validasi Token\n$manifestPath = __DIR__ . '/manifest_edit_tokens.json';\nif (!file_exists($manifestPath)) {\n die(\"",
"Pastikan cahaya terang",
"Periksa MAC filtering pada router",
"Silakan scan QR code yang valid dari admin untuk melakukan registrasi dan verifikasi wajah.",
"mengingat PIN",
"'error', 'message' => 'Sesi tidak valid. Silakan login ulang.']);\n exit;\n}\n\nif ($_SERVER['REQUEST_METHOD'] !== 'POST') {\n echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);\n exit;\n}\n\n$id_account = $_SESSION['user_id'];\n$form_type = $_POST['form_type'] ?? '';\n\n// Ambil data existing dulu agar tidak ter-overwrite dengan kosong\n$existing = $pdo->prepare(\"SELECT * FROM profile WHERE id_account = ?\");\n$existing->execute([$id_account]);\n$old = $existing->fetch(PDO::FETCH_ASSOC);\n\n// Merge: pakai POST value jika ada & tidak kosong, otherwise pakai existing\n$email = trim($_POST['email'] ?? ($old['email'] ?? ''));\n$moto = trim($_POST['moto'] ?? ($old['moto'] ?? ''));\n$provinsi = trim($_POST['provinsi'] ?? ($old['provinsi'] ?? ''));\n$kabupaten = trim($_POST['kabupaten'] ?? ($old['kabupaten'] ?? ''));\n$kecamatan = trim($_POST['kecamatan'] ?? ($old['kecamatan'] ?? ''));\n$desa = trim($_POST['desa'] ?? ($old['desa'] ?? ''));\n$kode_pos = trim($_POST['kode_pos'] ?? ($old['kode_pos'] ?? ''));\n$facebook = trim($_POST['facebook'] ?? ($old['facebook'] ?? ''));\n$instagram = trim($_POST['instagram'] ?? ($old['instagram'] ?? ''));\n$linkedin = trim($_POST['linkedin'] ?? ($old['linkedin'] ?? ''));\n$whatsapp = trim($_POST['whatsapp'] ?? ($old['whatsapp'] ?? ''));\n\n// Validasi berdasarkan form_type\nif ($form_type === 'social') {\n // Hanya validasi social media fields\n $required = [\n 'Facebook' => $facebook,\n 'Instagram' => $instagram,\n 'WhatsApp' => $whatsapp,\n ];\n foreach ($required as $label => $value) {\n if (empty($value)) {\n echo json_encode(['status' => 'error', 'message' => \"$label wajib diisi.\"]);\n exit;\n }\n }\n $waClean = preg_replace('/[^0-9]/', '', $whatsapp);\n if (strlen($waClean)",
"Data terverifikasi dan tersimpan",
"';\n el.classList.remove('msg-sending');\n }\n }\n },\n error: function() {\n let el = document.getElementById(tempId);\n if (el) {\n el.querySelector('.msg-status').innerHTML = '",
"`;}\n el('gPow').innerHTML=h;\n try{if(ps)initCh('cPS',ps);}catch(e){console.warn(e);}\n try{if(sd)initCh('cSD',sd);}catch(e){console.warn(e);}\n try{if(bt)initCh('cBT',bt);}catch(e){console.warn(e);}\n}\nfunction uPow(){\n const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');\n if(ps){const e=ps.extra_data||{};el('pV').textContent=fm(e.voltage_out)+' V';el('pA').textContent=fm(e.current_out)+' A';el('pW').textContent=fm(e.power_watts)+' W';el('info-ps').innerHTML=mr('mdi-flash','V In',fm(e.voltage_in)+' V','o')+mr('mdi-flash','V Out',fm(e.voltage_out)+' V','o');el('bar-ps').className='cd-top '+sc(ps.status);el('ft-ps').innerHTML='",
"' + (d.message || 'Gagal menyimpan ke server');\n errorEl.style.display = \"block\";\n btn.disabled = false;\n }\n }).catch(e => {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"Ya, Keluar",
"Save Data",
"`;}\n if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';\n h+=`",
"Monitor dan kelola sistem IoT Smart Door Anda dari sini.",
"Sidik jari berhasil ditambahkan.",
"Anda dapat menutup halaman ini.",
"Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.",
".auth .brand-logo img { width: 140px; max-width: 100%; }\n .auth-form-light { border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }\n .custom-alert {\n position: fixed; top: 20px; right: 20px; background: #fff;\n border-left: 4px solid #fcd34d; box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px; border-radius: 4px; z-index: 9999;\n display: none; align-items: center; gap: 12px; min-width: 300px;\n max-width: 400px; animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }\n @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: #1e293b; }\n .submit-btn {\n background: var(--primary); color: #fff; width: 100%; padding: 16px; border-radius: 16px; font-size: 16px; font-weight: 700; border: none; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 10px;\n }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }\n @media (max-width: 480px) {\n .custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }\n }\n .success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }\n @keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }\n \n .foto-wrapper:hover .foto-circle { border-color: #4747a1 !important; }\n .foto-wrapper:active { transform: scale(0.95); }",
"let currentOccupantId = 0;\n\nfunction fetchContacts() {\n $.post('chat_api.php', { action: 'get_contacts' }, function(res) {\n if (res.status === 'success') {\n let html = '';\n res.data.forEach(c => {\n let isActive = c.id_occupant == currentOccupantId ? 'active' : '';\n let lastMsg = c.last_message ? c.last_message : 'Belum ada pesan';\n let unread = c.unread_count > 0 ? `",
"utama dalam semua state (apapun yang terjadi) */\n .sidebar .nav .nav-item,\n .sidebar .nav .nav-item.active,\n .sidebar .nav .nav-item:hover,\n .sidebar .nav .nav-item.hover-open {\n background: transparent !important;\n }\n \n /* Untuk Identia dark/light theme khusus */\n .sidebar-light-theme .sidebar .nav .nav-item,\n .sidebar-light-theme .sidebar .nav .nav-item.active {\n background: transparent !important;\n }\n \n /* 2. EKSTRIM: Pindahkan background biru HANYA ke kotakan tombol Menu Utama SAJA (saat Active / Open) */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] {\n background: transparent !important; \n }\n \n .sidebar .nav .nav-item.active,\n .sidebar .nav .nav-item.active[aria-expanded=\"true\"],\n .sidebar-light-theme .sidebar .nav .nav-item.active,\n .sidebar-light-theme .sidebar .nav .nav-item.active[aria-expanded=\"true\"] {\n background: transparent !important; \n }\n \n /* Aturan Utama! Background biru HANYA pada .nav-link, BUKAN pada .nav-item */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"],\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"],\n .sidebar-light-theme .sidebar .nav .nav-item.active > .nav-link,\n .sidebar-light-theme .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"] {\n background: #4B49AC !important;\n margin-bottom: 0 !important;\n \n border-radius: 8px !important;\n \n overflow: hidden !important;\n box-shadow: none !important;\n border: none !important;\n clip-path: inset(0px round 8px) !important;\n }\n\n /* ========================================================= */\n /* FIX: Sidebar minimized (icon-only) mode */\n /* ========================================================= */\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link {\n display: block !important;\n text-align: center !important;\n padding-left: .5rem !important;\n padding-right: .5rem !important;\n position: static !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-icon {\n display: block !important;\n margin: 0 auto !important;\n width: auto !important;\n min-width: auto !important;\n font-size: 18px !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title,\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .badge,\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-sub-title {\n display: none !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-arrow {\n display: none !important;\n }\n /* Active state: full width, no radius */\n .sidebar-icon-only .sidebar .nav .nav-item.active > .nav-link,\n .sidebar-icon-only .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"],\n .sidebar-icon-only .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] {\n border-radius: 0 !important;\n clip-path: none !important;\n }\n /* Hover: subtle background, no white text on transparent */\n .sidebar-icon-only .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link {\n background: #4B49AC !important;\n border-radius: 0 !important;\n clip-path: none !important;\n }\n .sidebar-icon-only .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-icon {\n color: #fff !important;\n }\n /* Hover-open menu title popup */\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title {\n display: flex !important;\n align-items: center !important;\n background: #4B49AC !important;\n color: #fff !important;\n padding: 0.5rem 1.4rem !important;\n left: 70px !important;\n position: absolute !important;\n text-align: left !important;\n top: 0 !important;\n bottom: 0 !important;\n width: 190px !important;\n z-index: 1 !important;\n line-height: 1.8 !important;\n border-radius: 0 5px 5px 0 !important;\n box-shadow: 4px 0px 7px 0px rgba(0,0,0,0.08) !important;\n }\n /* Hover-open submenu popup */\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapse,\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapsing {\n display: block !important;\n background: #fff !important;\n border-radius: 0 0 5px 0 !important;\n position: absolute !important;\n left: 70px !important;\n width: 190px !important;\n box-shadow: 4px 4px 7px 0px rgba(0,0,0,0.08) !important;\n }\n \n /* 3. Pastikan teks dan ikon Menu Utama tetap berwarna putih jika aktif */\n .sidebar .nav .nav-item.active > .nav-link .menu-title,\n .sidebar .nav .nav-item.active > .nav-link i.menu-icon,\n .sidebar .nav .nav-item.active > .nav-link i.menu-arrow,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] .menu-title,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] i.menu-icon,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] i.menu-arrow {\n color: #fff !important;\n }\n \n /* 4. Reset background Container Sub-menu agar SELALU transparan di kondisi APAPUN */\n .sidebar .nav .nav-item .collapse,\n .sidebar .nav .nav-item .collapsing,\n .sidebar .nav .nav-item .sub-menu {\n background: transparent !important;\n }\n \n /* 5. Setel Teks Sub-menu kembali ke abu-abu gelap normal di semua kondisi */\n .sidebar .nav .nav-item .sub-menu .nav-item .nav-link,\n .sidebar .nav .nav-item .collapse .sub-menu .nav-item .nav-link {\n color: #6c7383 !important;\n background: transparent !important;\n font-weight: 400 !important;\n }\n \n /* Matikan gaya titik peluru list bawaan HTML */\n .sidebar .nav .sub-menu {\n list-style: none !important;\n }\n .sidebar .nav .sub-menu .nav-item {\n list-style-type: none !important;\n }\n .sidebar .nav .sub-menu .nav-item::before {\n display: none !important; \n content: \"\" !important;\n }\n \n /* 6. Matikan pewarnaan khusus peluru (.) bawaan .active agar kembali normal */\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link::before,\n .sidebar .nav .nav-item.active .collapse .sub-menu .nav-item .nav-link::before {\n content: \"\" !important; /* Hapus content kotak/icon jelek */\n color: #a3a4a5 !important;\n }\n /* Restorasi bentuk bulat bawaan Identia via ::before di tag \\",
"Token tidak ditemukan.",
"Menunggu Sensor...';\n statusEl.style.display = \"block\";\n \n if (!window.PublicKeyCredential) {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"';\n res.data.forEach(d=>{opts+=`",
"Data Berhasil diperbarui!",
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_occupant', id);\n return fetch('delete_occupant.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_occupant.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteOccupant\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n // Start timer when modal opens\n $('#modalCodeRegister').on('shown.bs.modal', function(){ startCodeRegisterTimer(); });\n // Stop timer + reload when modal closes\n $('#modalCodeRegister').on('hidden.bs.modal', function(){ stopCodeRegisterTimer(); location.reload(); });\n $('#modalTambah').on('hidden.bs.modal', function(){ document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"passwordMismatch\").style.display = \"none\"; });\n $('#modalEdit').on('hidden.bs.modal', function(){ document.getElementById(\"editPasswordMismatch\").style.display = \"none\"; });\n $('#modalEditFoto').on('hidden.bs.modal', function(){ location.reload(); });\n\n // DataTable\n if ($.fn.DataTable.isDataTable('#myTable')) { $('#myTable').DataTable().destroy(); }\n var table = $('#myTable').DataTable({ \"columnDefs\": [{ \"orderable\": false, \"targets\": 5 }, { \"width\": \"40px\", \"targets\": 5 }], \"order\": [[0, 'asc']] });\n\n // Child row\n $('#myTable tbody').on('click', 'td.details-control', function(e) {\n if(e.target.tagName === 'INPUT') return; // Let checkbox work\n var tr = $(this).closest('tr'), row = table.row(tr);\n if (row.child.isShown()) { row.child.hide(); tr.removeClass('shown'); return; }\n table.rows('.shown').every(function() { this.child.hide(); $(this.node()).removeClass('shown'); });\n\n var d = { id: $(this).data(\"id\"), name: $(this).data(\"name\"), nik: $(this).data(\"nik\"), no_hp: $(this).data(\"no_hp\"), foto: $(this).data(\"foto\"), hasfoto: $(this).data(\"hasfoto\"), fingerid: $(this).data(\"fingerid\"), fingerok: $(this).data(\"fingerok\"), code: $(this).data(\"code\"), progress: $(this).data(\"progress\"), status: $(this).data(\"status\"), access: $(this).data(\"access\") };\n\n var progPill = d.progress === 'used' ? '",
"Ini bukanlah Bot otomatis yang bisa menjawab pesan secara otomatis",
"Upload Foto",
"— pastikan antena router menghadap vertikal (tegak). Untuk jangkauan multi-lantai, miringkan satu antena horizontal dan satu vertikal.',\n '",
"Coba dari perangkat lain",
"\" style=\"border-radius:6px; font-weight:bold; width:32px; padding: 4px 0; font-size:14px; display:inline-flex; align-items:center; justify-content:center;\" disabled title=\"Tekan untuk menyimpan perubahan\">",
"Gunakan sensor biometrik perangkat Anda saat ini.",
"Menyimpan...';\n \n fetch(\"edit_account.php\", { method: \"POST\", body: new FormData(this) })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
"Aktivitas sistem dan peringatan keamanan",
"15 detik namun tidak mendapat respons",
"Format email tidak valid';\n fb.className = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? 'pf-fb valid' : 'pf-fb invalid';\n});\n\ndocument.getElementById('edit_whatsapp').addEventListener('blur', function() {\n const fb = document.getElementById('whatsappFeedback');\n const v = this.value.trim().replace(/[^0-9]/g, '');\n if (!v) { fb.className = 'pf-fb'; return; }\n fb.innerHTML = (v.length >= 10 && v.length",
"Foto wajah Anda telah berhasil tersimpan. Anda dapat menutup halaman ini.",
"(async function() {\n var MODEL_URLS = 'js/face-api/models';\n \n var pgCamera = document.getElementById('pgCamera');\n var pgSuccess = document.getElementById('pgSuccess');\n var camLoading = document.getElementById('camLoading');\n var loadProgress = document.getElementById('loadProgress');\n var loadText = document.getElementById('loadText');\n var stPill = document.getElementById('stPill');\n var stIcon = document.getElementById('stIcon');\n var stText = document.getElementById('stText');\n var vid = document.getElementById('vid');\n var ovCanvas = document.getElementById('ovCanvas');\n var refImg = document.getElementById('refImg');\n var oval = document.getElementById('oval');\n var distFill = document.getElementById('distFill');\n \n var brightMeter = document.getElementById('brightMeter');\n var bmFill = document.getElementById('bmFill');\n var bmIcon = document.getElementById('bmIcon');\n var liveMeter = document.getElementById('liveMeter');\n var lmFill = document.getElementById('lmFill');\n var lmIcon = document.getElementById('lmIcon');\n \n var refDescriptor = null;\n var stream = null;\n var detectionLoopInterval = null;\n var brightInterval = null;\n \n var livenessScore = 0;\n var livenessFrames = 0;\n var prevLandmarks = null;\n \n function setProgress(pct) { loadProgress.style.width = pct + '%'; }\n function setStatus(type, txt) {\n stPill.className = 'st-pill';\n if (type === 'search') { stPill.classList.add('searching'); stIcon.className = 'mdi mdi-face-recognition'; }\n if (type === 'ok') { stPill.classList.add('found'); stIcon.className = 'mdi mdi-check-circle'; }\n if (type === 'err') { stPill.classList.add('warn'); stIcon.className = 'mdi mdi-alert-circle'; }\n stText.textContent = txt;\n }\n\n try {\n setProgress(20);\n loadText.textContent = 'Memuat model AI...';\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URLS),\n faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URLS),\n faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URLS)\n ]);\n \n setProgress(50);\n loadText.textContent = 'Menghitung ciri wajah referensi...';\n \n // Wait for image to load before processing\n if (!refImg.complete || refImg.naturalWidth === 0) {\n await new Promise((resolve, reject) => {\n refImg.onload = resolve;\n refImg.onerror = () => reject(new Error(\"Failed memuat foto referensi dari database.\"));\n if (refImg.complete && refImg.naturalWidth === 0) {\n reject(new Error(\"Foto referensi rusak atau format tidak valid.\"));\n }\n });\n }\n \n // We relax the score threshold for the reference image in case of lighting/angle issues\n const refDetection = await faceapi.detectSingleFace(refImg, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.1 })).withFaceLandmarks().withFaceDescriptor();\n if (!refDetection) {\n alert('Tidak dapat mendeteksi wajah pada foto referensi. Mohon perbarui foto occupant.');\n window.location.href = 'new_occupant.php';\n return;\n }\n refDescriptor = refDetection.descriptor;\n \n setProgress(70);\n loadText.textContent = 'Memulai kamera...';\n \n if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n throw new Error(\"Kamera tidak bisa diakses. Browser mungkin meminta HTTPS.\");\n }\n var isMobile = window.innerWidth",
"=this.history.length-1)return;this.historyIdx++;this.restoreHistory();}\n\n restoreHistory(){\n this.screens=JSON.parse(this.history[this.historyIdx]);\n if(this.currentScreen>=this.screens.length)this.currentScreen=0;\n this.selectedId=null;\n this.renderCanvas();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.clearProps();\n }\n\n // ===== EXPORT JSON =====\n getDesignJSON(){\n return {projectName:document.getElementById('projectName').value,orientation:this.orientation,screens:this.screens};\n }\n\n // ===== API =====\n save(){\n const data=this.getDesignJSON();\n const fd=new FormData();\n fd.append('action','save');\n fd.append('id',this.designId||0);\n fd.append('name',data.projectName||'Untitled');\n fd.append('design_json',JSON.stringify(data));\n fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{\n if(res.status==='success'){this.designId=res.id;alert('✅ Saved!');this.loadList();}\n else alert('❌ '+res.message);\n });\n }\n\n loadList(){\n fetch('api_sync.php?action=list').then(r=>r.json()).then(res=>{\n if(res.status==='success'){\n let opts='",
"/* ── MANUAL STRIPES & BORDERS ── */\n #myTable { border-spacing: 0 12px !important; border-collapse: separate !important; }\n #myTable thead th { border-bottom: 1px solid #e2e8f0 !important; padding-bottom: 8px !important; }\n #myTable tbody tr td { border-bottom: none !important; padding-top: 6px !important; padding-bottom: 6px !important; }\n\n .pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n .pill-green { background: #e8f5e9; color: #2e7d32; }\n .pill-red { background: #ffebee; color: #c62828; }\n .pill-amber { background: #fff8e1; color: #f57f17; }\n .pill-gray { background: #f5f5f5; color: #757575; }\n\n .mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n .confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n .confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n .delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n .frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n .pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n .pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n\n .eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n .eye-toggle:hover i { color: #4b7bec; }\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n\n /* Button Pulse Animation */\n @keyframes pulse-green {\n 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }\n 70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }\n }\n .pulse-btn {\n animation: pulse-green 2s infinite;\n }\n\n /* Scanner Animation */\n @keyframes scan {\n 0% { top: 0; opacity: 0; }\n 10% { opacity: 1; }\n 90% { opacity: 1; }\n 100% { top: 100%; opacity: 0; }\n }\n .scanning {\n animation: scan 2s cubic-bezier(0.53, 0.21, 0.29, 0.67) infinite;\n }\n\n /* Modal Tweaks */\n .modal-backdrop.show {\n opacity: 0.6;\n backdrop-filter: blur(4px);\n }",
"Posisikan wajah dalam bingkai oval",
"const video = document.getElementById('camera-feed');\n const canvas = document.getElementById('photo-canvas');\n const overlay = document.getElementById('canvas-overlay');\n const btnCapture = document.getElementById('btn-capture');\n const btnRetake = document.getElementById('btn-retake');\n const locText = document.getElementById('loc-text');\n const timeText = document.getElementById('time-text');\n const photoData = document.getElementById('photo_data');\n const liveMetadata = document.getElementById('live-metadata');\n const scanLine = document.getElementById('scan-line');\n const faceBox = document.getElementById('face-box');\n const faceLabel = document.getElementById('face-label');\n\n let currentStream = null;\n let placeName = \"Lokasi belum didapat\";\n\n setTimeout(() => {\n faceLabel.innerText = \"Wajah Terdeteksi\";\n faceLabel.style.color = \"#fff\";\n faceLabel.style.background = \"#10b981\"; // success green\n faceBox.style.borderColor = \"#10b981\";\n }, 3000);\n\n async function startCamera() {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user' },\n audio: false\n });\n currentStream = stream;\n video.srcObject = stream;\n } catch (err) {\n locText.innerText = \"Kamera tidak tersedia.\";\n document.querySelector('.cam-loader').innerText = \"Kamera tidak aktif.\";\n scanLine.style.display = 'none';\n faceBox.style.display = 'none';\n }\n }\n\n function getLocation() {\n if (navigator.geolocation) {\n navigator.geolocation.getCurrentPosition(\n async (position) => {\n let lat = position.coords.latitude;\n let lon = position.coords.longitude;\n locText.innerText = `Memproses lokasi...`;\n try {\n let res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`);\n let data = await res.json();\n if (data && data.display_name) {\n placeName = data.display_name.split(\",\").slice(0, 3).join(\",\");\n locText.innerText = placeName;\n }\n } catch (e) {\n placeName = `GPS: ${lat.toFixed(4)}, ${lon.toFixed(4)}`;\n locText.innerText = placeName;\n }\n },\n (error) => {\n locText.innerText = \"Izin lokasi ditolak\";\n }\n );\n } else {\n locText.innerText = \"GPS tidak didukung\";\n }\n }\n\n function updateTime() {\n const now = new Date();\n const options = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };\n // avoid timezone text wrapping issues or missing\n timeText.innerText = now.toLocaleDateString('id-ID', options) + ' ' + now.toLocaleTimeString('id-ID');\n }\n\n btnCapture.addEventListener('click', () => {\n if (!video.videoWidth) return;\n\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n const ctx = canvas.getContext('2d');\n \n ctx.translate(canvas.width, 0);\n ctx.scale(-1, 1);\n ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n \n ctx.translate(canvas.width, 0);\n ctx.scale(-1, 1);\n \n ctx.fillStyle = \"rgba(0, 0, 0, 0.5)\"; \n ctx.fillRect(0, canvas.height - 100, canvas.width, 100);\n \n let fontSize = Math.max(14, Math.floor(canvas.width * 0.035));\n ctx.font = `${fontSize}px Arial`;\n ctx.fillStyle = \"white\";\n ctx.textBaseline = \"middle\";\n \n const currentTime = new Date().toLocaleString('id-ID');\n ctx.fillText(`📍 ${placeName}`, 20, canvas.height - 70);\n ctx.fillText(`📅 ${currentTime}`, 20, canvas.height - 40);\n \n ctx.fillStyle = \"#34d399\";\n ctx.fillText(`Verified: ${document.getElementById('occupant_id').value}`, 20, canvas.height - 15);\n \n const dataURL = canvas.toDataURL('image/jpeg', 0.85);\n overlay.src = dataURL;\n overlay.style.display = 'block';\n \n video.style.display = 'none';\n btnCapture.style.display = 'none';\n liveMetadata.style.display = 'none';\n scanLine.style.display = 'none';\n faceBox.style.display = 'none';\n btnRetake.style.display = 'inline-block';\n \n photoData.value = dataURL;\n });\n\n btnRetake.addEventListener('click', () => {\n overlay.style.display = 'none';\n video.style.display = 'block';\n btnCapture.style.display = 'flex';\n liveMetadata.style.display = 'flex';\n scanLine.style.display = 'block';\n faceBox.style.display = 'flex';\n btnRetake.style.display = 'none';\n photoData.value = '';\n });\n\n document.getElementById('reportForm').addEventListener('submit', (e) => {\n if(!photoData.value) {\n alert('Silakan ambil foto kegiatan terlebih dahulu!');\n e.preventDefault();\n }\n });\n\n startCamera();\n getLocation();\n setInterval(updateTime, 1000);",
"Mendaftar sidik jari untuk login lebih cepat tanpa password.",
"// ── Full Camera System (registrasi_foto.php style) ──\n let editStream = null, editDetectLoop = null, editFaceOk = false, editDataURL = null;\n let editBrightInterval = null, editLivenessScore = 0;\n let editPrevLandmarks = null, editLivenessFrames = 0;\n let editModelsLoaded = false;\n\n const editVid = document.getElementById('editVid');\n const editOvCanvas = document.getElementById('editOvCanvas');\n const editOval = document.getElementById('editOval');\n const editCamLoading = document.getElementById('editCamLoading');\n const editStPill = document.getElementById('editStPill');\n const editStIcon = document.getElementById('editStIcon');\n const editStText = document.getElementById('editStText');\n const editBtnShutter = document.getElementById('editBtnShutter');\n const editCamArea = document.getElementById('editCamArea');\n const editPreviewWrap = document.getElementById('editPreviewWrap');\n const editCapCanvas = document.getElementById('editCapCanvas');\n const editBarAfter = document.getElementById('editBarAfter');\n const editBmFill = document.getElementById('editBmFill');\n const editBmIcon = document.getElementById('editBmIcon');\n const editLmFill = document.getElementById('editLmFill');\n const editLmIcon = document.getElementById('editLmIcon');\n const editLoadProgress = document.getElementById('editLoadProgress');\n const editLoadText = document.getElementById('editLoadText');\n\n function editSetProgress(pct) { editLoadProgress.style.width = Math.min(100, pct) + '%'; }\n function editSetStatus(type, txt) {\n editStPill.className = 'ecam-st-pill';\n if (type === 'search') { editStPill.classList.add('searching'); editStIcon.className = 'mdi mdi-face-recognition'; }\n if (type === 'ok') { editStPill.classList.add('found'); editStIcon.className = 'mdi mdi-check-circle'; }\n if (type === 'err') { editStPill.classList.add('warn'); editStIcon.className = 'mdi mdi-alert-circle'; }\n editStText.textContent = txt;\n }\n\n function startCamera() {\n document.getElementById('cameraOverlay').style.display = 'flex';\n editCamLoading.style.display = 'flex';\n editLoadText.textContent = 'Memuat kamera & model deteksi wajah...';\n editSetProgress(5);\n editCamArea.style.display = 'block';\n editPreviewWrap.style.display = 'none';\n editBarAfter.style.display = 'none';\n document.querySelector('.ecam-hud').style.display = 'flex';\n editFaceOk = false;\n editDataURL = null;\n editLivenessScore = 0;\n editLivenessFrames = 0;\n editPrevLandmarks = null;\n editLmFill.style.height = '0%';\n\n initEditCamera();\n }\n\n async function initEditCamera() {\n try {\n if (!editModelsLoaded) {\n editSetProgress(20);\n editLoadText.textContent = 'Memuat model deteksi wajah...';\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),\n faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models')\n ]);\n editModelsLoaded = true;\n }\n editSetProgress(50);\n editLoadText.textContent = 'Memulai kamera...';\n\n const isMobile = window.innerWidth",
"`;}\n if(bz){const e=bz.extra_data||{};const bp=e.buzzer_state==='beeping';\n h+=`",
"\"\n data-time=\"",
"Tambah Occupant",
"Catatan: Setelah berhasil, Anda dapat mengakses menu bantuan dengan mengetik /help. Menu tersebut menyediakan akses mandiri secara ringkas untuk:",
"Loginkan Username Anda",
"Password Baru (Opsional)",
"SET PIN",
"Sistem otomatis merekam kehadiran Anda. Silakan foto dan deskripsikan kegiatan Anda.",
"yang sah.\\n\\n\"\n . \"Saat ini, Anda sudah",
"Coba Lagi",
"Hapus Permanen?",
"Jangan bagikan password ini kepada siapapun!",
"Pilih Occupant",
"Pilih Bulan",
"QR hanya bisa digunakan",
"Klik ikon di atas untuk generate QR Code",
"Form Tidak Lengkap",
"LinkedIn belum diisi",
"'+tA(so.last_seen);}\n if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';el('info-rl').innerHTML=`",
"\"\n data-foto=\"",
"';\n };\n xhr.send(fd);\n }\n\n // ── Events ──\n if (btnShutter) btnShutter.addEventListener('click', capture);\n if (btnRetake) btnRetake.addEventListener('click', retake);\n if (btnSave) btnSave.addEventListener('click', save);\n\n // ── Init ──\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', validateToken);\n } else {\n validateToken();\n }\n\n // ── PWA Service Worker ──\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('sw_foto.js').catch(function() {});\n }\n\n // ── Smooth instruction fade ──\n var ip = document.getElementById('instrPanel');\n if (ip) ip.style.transition = 'opacity 0.5s ease';\n})();",
"Verifikasi Berhasil!",
"Pesan ini akan dihapus dalam 5 detik...",
".edit-cam-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:9999; background:#1e2223; flex-direction:column; font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif; }\n .edit-cam-overlay * { box-sizing:border-box; }\n .ecam-header { display:flex; justify-content:space-between; align-items:center; padding:20px; z-index:30; position:absolute; top:0; left:0; right:0; }\n .ecam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .ecam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .ecam-area canvas.ecam-overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n .ecam-face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:10; }\n .ecam-face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n .ecam-face-oval #editOvalRing { stroke:white; stroke-width:3; transition:all 0.4s ease; }\n .ecam-face-oval.ok #editOvalRing { stroke:#4747a1; stroke-width:5; filter:drop-shadow(0 0 12px rgba(71,71,161,0.4)); }\n .ecam-face-oval #editOvalRingOut { stroke:rgba(255,255,255,0.4); stroke-width:8; transition:all 0.4s ease; }\n .ecam-face-oval.ok #editOvalRingOut { stroke:rgba(71,71,161,0.4); }\n .ecam-loading { position:absolute; inset:0; background:rgba(255,255,255,0.95); backdrop-filter:blur(10px); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:25; gap:20px; }\n .ecam-loading .ecam-ldring { width:48px; height:48px; border-radius:50%; border:3px solid #e6e6f2; border-top-color:#4747a1; animation:ecamSpin 0.8s cubic-bezier(0.4,0,0.2,1) infinite; }\n @keyframes ecamSpin { to { transform:rotate(360deg); } }\n .ecam-loading p { font-size:14px; color:#64748b; font-weight:600; text-align:center; }\n .ecam-load-progress { width:200px; height:6px; background:rgba(71,71,161,0.15); border-radius:4px; overflow:hidden; }\n .ecam-load-progress-fill { height:100%; width:0%; background:#4747a1; transition:width 0.4s ease; border-radius:4px; }\n .ecam-st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; z-index:12; white-space:nowrap;\n background:rgba(255,255,255,0.85); border:1px solid rgba(255,255,255,0.5); color:#0f172a;\n backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .ecam-st-pill i { font-size:18px; }\n .ecam-st-pill.searching { color:#64748b; }\n .ecam-st-pill.searching i { color:#4747a1; }\n .ecam-st-pill.found { background:#10b981; color:#fff; border-color:#10b981; }\n .ecam-st-pill.found i { color:#fff; }\n .ecam-st-pill.warn { background:#ef4444; color:#fff; border-color:#ef4444; }\n .ecam-st-pill.warn i { color:#fff; }\n .ecam-hud { position:absolute; bottom:0; left:0; right:0; z-index:20; display:flex; flex-direction:column; align-items:center; padding-bottom:30px; pointer-events:none; }\n .ecam-shutter { width:76px; height:76px; border-radius:50%; pointer-events:auto; border:4px solid rgba(255,255,255,0.4); background:rgba(255,255,255,0.1); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.3s; position:relative; backdrop-filter:blur(4px); }\n .ecam-shutter::after {\n content:''; width:56px; height:56px; border-radius:50%;\n background:#fff; transition:all 0.2s;\n }\n .ecam-shutter:active::after { transform:scale(0.8); background:#4747a1; }\n .ecam-shutter:disabled { opacity:0.6; cursor:not-allowed; pointer-events:none; }\n .ecam-preview { flex:1; position:relative; background:#1e2223; overflow:hidden; display:none; }\n .ecam-preview canvas { width:100%; height:100%; object-fit:cover; }\n .ecam-preview-tag { position:absolute; top:85px; left:50%; transform:translateX(-50%); background:#4747a1; color:#fff; padding:10px 20px; border-radius:30px; font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px; z-index:5; box-shadow:0 4px 15px rgba(71,71,161,0.4); }\n .ecam-bar-after { position:absolute; bottom:0; left:0; right:0; z-index:40; padding:30px 0; background:rgba(255,255,255,0.95); backdrop-filter:blur(10px); display:none; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226,232,240,0.8); border-top-left-radius:24px; border-top-right-radius:24px; pointer-events:auto; }\n .ecam-act-btn { display:flex; flex-direction:column; align-items:center; gap:8px; border:none; background:none; cursor:pointer; transition:0.2s; pointer-events:auto; outline:none; }\n .ecam-act-btn .ring { width:56px; height:56px; border-radius:18px; display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.2s; }\n .ecam-act-btn span { font-size:11px; font-weight:700; color:#0f172a; text-transform:uppercase; letter-spacing:0.5px; }\n .ecam-act-btn.retry .ring { background:#f1f5f9; color:#64748b; border:1px solid #e2e8f0; }\n .ecam-act-btn.save .ring { background:#4747a1; color:#fff; }\n .ecam-bm, .ecam-lm { position:absolute; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12; padding:14px 10px; background:none; border-radius:0; backdrop-filter:none; }\n .ecam-bm { right:6px; }\n .ecam-lm { left:6px; }\n .ecam-bm i, .ecam-lm i { font-size:16px; color:#fff; }\n .ecam-meter-bar { width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; border-radius:6px; }\n .ecam-meter-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:6px; background:#fff; transition:height 0.4s cubic-bezier(0.4,0,0.2,1), background-color 0.4s ease; }",
"');\n $('#esp32-ssid').text('Unknown');\n $('#esp32-ip').text('Unknown');\n \n } else {\n // Offline\n isWifiConnected = false;\n $('#wifi-status-text').text('WiFi Disconnected').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#ef4444', 'box-shadow': 'none'});\n \n $('#esp32-status-icon-container').html('",
"Tandai Semua Dibaca",
"'+tA(bz.last_seen);}\n if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lg').innerHTML=`",
"VERIFY PIN",
"Pastikan pencahayaan cukup terang",
"Verifikasi Wajah — Identia",
"— jika SSID disembunyikan (hidden network) di pengaturan router, aktifkan broadcast SSID terlebih dahulu agar ESP32 dapat menemukannya.',\n '",
"Mencocokkan identitas wajah...",
"Berhasil",
"Gagal terkirim",
"Pilih Chat",
"\";\n \n // Meminta kontak dari user menggunakan ReplyKeyboardMarkup\n $keyboard = [\n 'keyboard' => [\n [\n ['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true]\n ]\n ],\n 'resize_keyboard' => true,\n 'is_persistent' => true\n ];\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($keyboard)\n ];\n \n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n exit;\n }\n \n // Konfirmasi / Cek Nomor HP\n if (isset($update[\"message\"][\"contact\"])) {\n $phoneNumber = $update[\"message\"][\"contact\"][\"phone_number\"];\n \n // Normalisasi nomor HP (telegram biasa mengirim format internasional, misal 628... atau +628...\n // Kita ubah menjadi 08... menyesuaikan format lokal\n $normalizedPhone = preg_replace('/^\\+?62/', '0', $phoneNumber);\n \n // Cek database di tabel occupant\n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? OR no_hp = ? OR no_hp = ?\");\n $stmt->execute([\n $phoneNumber, \n $normalizedPhone, \n \"62\" . ltrim($normalizedPhone, '0')\n ]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n // Nomor terdaftar, simpan chat_id ke database\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $updateStmt = $pdo->prepare(\"UPDATE occupant SET telegram_id = ? WHERE id_occupant = ?\");\n $updateStmt->execute([$chatId, $occupant['id_occupant']]);\n \n $text = \"✅",
"Silakan klik button",
"Simpan",
"';\n \n $.post('chat_api.php', { action: 'get_messages', id_occupant: id }, function(res) {\n if(res.status === 'success') {\n document.getElementById('activeName').innerText = res.occupant.name;\n let occStatStr = 'Status tidak diketahui';\n if (res.occupant.last_active) {\n let lastActStr = res.occupant.last_active.replace(/-/g, '/');\n let actDate = new Date(lastActStr);\n let diffSec = (new Date() - actDate) / 1000;\n let diffDays = diffSec / (60 * 60 * 24);\n \n if (diffSec",
"Kosongkan jika tidak ingin mengubah foto.",
"Cek profil",
".\\n\\n\"\n . \"Penting untuk diketahui:",
"7 days)\ntry {\n // Karena kita tidak punya timestamp khusus 'kapan dibaca', kita hitung dari waktu notifikasi dibuat (time) + sudah is_read\n $pdo->exec(\"UPDATE notifications SET is_deleted = 1, deleted_at = NOW() WHERE is_deleted = 0 AND is_read = 1 AND time",
"Menyimpan...';\n btn.disabled = true;\n document.getElementById('btnBack').disabled = true;\n\n const form = document.getElementById('editForm');\n const formData = new FormData(form);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n // Success\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step3-success').style.display = 'block';\n \n let countdown = 10;\n const cText = document.getElementById('countdownText');\n setInterval(() => {\n countdown--;\n cText.innerText = countdown;\n if(countdown",
"' + msg;\n toast.style.display = 'flex';\n }\n\n function setProgress(pct) {\n if (loadProgress) loadProgress.style.width = Math.min(100, pct) + '%';\n }\n\n\n\n // ── Anti-screenshot / right-click ──\n document.addEventListener('contextmenu', function(e) { e.preventDefault(); });\n\n // ── Step 1: Validate Token ──\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n startTokenCheck();\n } else {\n document.getElementById('errTitle').textContent = 'Link Invalid';\n document.getElementById('errMsg').textContent = \n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Link tautan ini sudah pernah digunakan atau kadaluwarsa. Silakan minta link terbarunya.' : 'Link tidak valid atau sesi berakhir.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Failed';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n // ── Periodic token validity check ──\n function startTokenCheck() {\n tokenCheckInterval = setInterval(function() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (false && !sessionSaved) {\n clearInterval(tokenCheckInterval);\n stopEverything();\n document.getElementById('errTitle').textContent = 'Token Kadaluwarsa';\n document.getElementById('errMsg').textContent = d.message || 'Token sudah tidak valid.';\n showPage(pgError);\n }\n } catch(e) {}\n };\n xhr.send();\n }, 30000);\n }\n\n function stopEverything() {\n clearInterval(detectLoop);\n clearInterval(brightInterval);\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n }\n\n // ── Step 2: Load models + camera ──\n var modelLoaded = false, landmarkLoaded = false, cameraStarted = false;\n\n function initCamera() {\n setProgress(5);\n loadText.textContent = 'Memuat library deteksi wajah...';\n\n // face-api.js loaded locally via",
"Hanya 1 wajah saja",
"');\n formData.append('credential_id', credId);\n\n const res = await fetch('save_fingerprint_account.php', {\n method: 'POST',\n body: formData\n });\n const result = await res.json();\n\n if (result.status === 'success') {\n showCustomAlert(\"Berhasil\", \"Sidik jari berhasil didaftarkan. Anda dapat menggunakannya untuk login.\", \"success\");\n setTimeout(() => { window.location.href = 'login.php'; }, 2000);\n } else {\n showCustomAlert(\"Gagal\", \"Gagal dari server: \" + result.message, \"error\");\n }\n }\n } catch(err) {\n console.error(err);\n showCustomAlert(\"Dibatalkan\", \"Proses dibatalkan atau sidik jari gagal dibaca.\", \"warning\");\n }\n });",
"Masukkan PIN 6 digit untuk verifikasi akses.",
"Menyimpan...';\n fetch(\"tambah_occupant.php\", { method: \"POST\", body: new FormData(this) }).then(function(r){ return r.json(); }).then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
"\";\n }\n }\n return \"[Attachment Failed]\";\n}\n\n// Hapus Webhook jika sebelumnya pernah diset (Webhook dan Polling tidak bisa bersamaan)\nrequest($website . \"/deleteWebhook\");\n\n$offset = 0;\n\nwhile (true) {\n // Ambil data (Long Polling)\n $response = request($website . \"/getUpdates\", [\n 'offset' => $offset,\n 'timeout' => 20 // Tahan koneksi sampai 20 detik jika tak ada pesan baru\n ]);\n\n if (!empty($response['ok']) && !empty($response['result'])) {\n foreach ($response['result'] as $update) {\n $offset = $update['update_id'] + 1; // Update offset agar pesan ini tidak ditarik lagi\n \n if (isset($update[\"message\"])) {\n $chatId = $update[\"message\"][\"chat\"][\"id\"];\n $username = isset($update[\"message\"][\"from\"][\"first_name\"]) ? $update[\"message\"][\"from\"][\"first_name\"] : \"User\";\n \n echo \"[\".date('Y-m-d H:i:s').\"] Pesan dari $username ($chatId): \";\n \n // Command /start\n if (isset($update[\"message\"][\"text\"]) && $update[\"message\"][\"text\"] === \"/start\") {\n echo \"/start\\n\";\n $text = \"Halo! Saya adalah *Identia*.\\n\\n\"\n . \"Penting untuk diketahui: *Ini bukanlah Bot otomatis yang bisa menjawab secara otomatis*. Ini adalah *saluran interaksi langsung* untuk terhubung secara personal dengan Admin Identia.\\n\\n\"\n . \"Namun, untuk memulai obrolan langsung dengan Admin, kami perlu memverifikasi data Anda terlebih dahulu.\\n\\n\"\n . \"👇 *Silakan klik tombol di bawah ini untuk Membagikan Kontak Anda* agar kami dapat mencocokkan nomor handphone Anda dengan sistem kami.\";\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'Markdown',\n 'reply_markup' => [\n 'keyboard' => [\n [ ['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true] ]\n ],\n 'resize_keyboard' => true,\n 'one_time_keyboard' => true\n ]\n ];\n request($website.\"/sendMessage\", $postData);\n continue; // lanjut ke update berikutnya\n }\n \n // Konfirmasi / Cek Nomor HP\n if (isset($update[\"message\"][\"contact\"])) {\n $phoneNumber = $update[\"message\"][\"contact\"][\"phone_number\"];\n echo \"[Share Contact: $phoneNumber]\\n\";\n \n $normalizedPhone = preg_replace('/^\\+?62/', '0', $phoneNumber);\n \n // Cek database di tabel occupant\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? OR no_hp = ? OR no_hp = ?\");\n $stmt->execute([\n $phoneNumber, \n $normalizedPhone, \n \"62\" . ltrim($normalizedPhone, '0')\n ]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $updateStmt = $pdo->prepare(\"UPDATE occupant SET telegram_id = ? WHERE id_occupant = ?\");\n $updateStmt->execute([$chatId, $occupant['id_occupant']]);\n \n $text = \"✅ *Verifikasi Berhasil!*\\n\\n\"\n . \"Nomor Anda telah terverifikasi sebagai Penghuni *Identia* yang sah.\\n\\n\"\n . \"Saat ini, Anda sudah *Terhubung Langsung* dengan saluran pribadi Admin *Identia*.\\n\\n\"\n . \"Silakan sampaikan pertanyaan, laporan kendala, atau pesan Anda di sini kapan pun Anda butuhkan. Admin akan membalas pesan Anda dari Control Panel sesegera mungkin.\";\n\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'Markdown',\n 'reply_markup' => ['remove_keyboard' => true]\n ];\n request($website.\"/sendMessage\", $postData);\n } else {\n $postData = [\n 'chat_id' => $chatId,\n 'text' => \"❌ Maaf, nomor handphone Anda ($phoneNumber) tidak ditemukan pada daftar penghuni Identia.\\n\\nPastikan Anda mendaftar melalui Admin terlebih dahulu.\"\n ];\n request($website.\"/sendMessage\", $postData);\n }\n continue;\n }\n \n // Handle Teks Biasa (Occupant membalas/chat ke Admin)\n if (isset($update[\"message\"])) {\n $msgObj = $update[\"message\"];\n $textMessage = \"\";\n $captionStr = isset($msgObj[\"caption\"]) ? htmlspecialchars($msgObj[\"caption\"]) : \"\";\n \n if (isset($msgObj[\"text\"])) {\n $textMessage = htmlspecialchars($msgObj[\"text\"]);\n echo \"Teks: $textMessage\\n\";\n } else {\n // Kalau bukan text biasa, kita abaikan saja agar tidak tersimpan di database\n continue; \n }\n \n if (!empty($textMessage)) {\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $id_account = 1; \n try { $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\"); } catch(PDOException $e) {}\n \n $replyToId = null;\n if (isset($msgObj['reply_to_message']) && isset($msgObj['reply_to_message']['message_id'])) {\n $tgReplyId = $msgObj['reply_to_message']['message_id'];\n // Temukan chat lokal berdasarkan telegram_msg_id\n $qRep = $pdo->prepare(\"SELECT id_chat FROM chats WHERE telegram_msg_id = ? LIMIT 1\");\n $qRep->execute([$tgReplyId]);\n $foundRep = $qRep->fetch();\n if ($foundRep) $replyToId = $foundRep['id_chat'];\n }\n \n $insertStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, is_read, telegram_msg_id, reply_to_chat_id) VALUES (?, ?, 'occupant', ?, 0, ?, ?)\");\n $insertStmt->execute([$id_account, $occupant['id_occupant'], $textMessage, $msgObj['message_id'], $replyToId]);\n \n // ALL PREVIOUS MESSAGES TO THIS USER ARE NOW READ\n try {\n $pdo->exec(\"UPDATE chats SET is_read = 1 WHERE id_occupant = {$occupant['id_occupant']} AND sender = 'account'\");\n } catch(PDOException $e) {}\n \n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\"); } catch(PDOException $e) {}\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$occupant['id_occupant']]);\n \n } else {\n $postData = [\n 'chat_id' => $chatId,\n 'text' => \"Sesi chat Anda belum terhubung dengan Identia.\\n\\nSilakan kirim perintah /start lalu klik tombol 'Bagikan Kontak Saya' terlebih dahulu untuk memulai obrolan dengan Admin.\"\n ];\n request($website.\"/sendMessage\", $postData);\n }\n }\n }\n }\n \n // Handle Pesan Diedit (Occupant mengedit pesan di Telegram)\n if (isset($update[\"edited_message\"])) {\n $chatId = $update[\"edited_message\"][\"chat\"][\"id\"];\n \n if (isset($update[\"edited_message\"][\"text\"])) {\n $editedText = $update[\"edited_message\"][\"text\"];\n $messageId = $update[\"edited_message\"][\"message_id\"];\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $newText = htmlspecialchars($editedText) . \"",
"Verifikasi';\n btn.disabled = false;\n \n if (d.status === \"success\") {\n $(\"#modalVerifyPin\").modal(\"hide\");\n document.getElementById(\"revealedPassword\").value = d.password;\n $(\"#modalShowPassword\").modal(\"show\");\n } else {\n document.getElementById(\"verifyPinErrorMsg\").innerText = d.message || \"PIN salah!\";\n document.getElementById(\"verifyPinError\").style.display = \"block\";\n }\n }, \"json\").fail(function() {\n btn.innerHTML = '",
"Edit Personal Information",
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol.",
"Pantau status perangkat secara real-time,",
"`;}\n}\n\n// POWER\nfunction bPow(){\n const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');let h='';\n if(ps){const e=ps.extra_data||{};\n h+=`",
"Tidak ada kontak.",
"Berhasil Terhubung!",
"QR Registrasi Foto",
"— router yang sudah berjalan lama mungkin mengalami memory leak atau channel congestion. Restart bisa menyelesaikan masalah ini.',\n '",
"— jika ESP32 harus ditempatkan jauh dari router, pasang repeater WiFi di titik tengah untuk memperkuat jangkauan sinyal.',\n '",
"document.getElementById('registerFinger').addEventListener('click', async () => {\n try {\n if (!window.PublicKeyCredential) {\n showCustomAlert(\"Tidak Didukung\", \"Browser ini tidak mendukung WebAuthn (Passkey / Sidik Jari).\", \"error\");\n return;\n }\n\n const publicKey = {\n challenge: new Uint8Array(32),\n rp: { \n name: \"Identia App\",\n id: window.location.hostname \n },\n user: {\n id: Uint8Array.from(\"uid_",
"exec(\"ALTER TABLE history \n ADD COLUMN is_read TINYINT(1) DEFAULT 0,\n ADD COLUMN is_starred TINYINT(1) DEFAULT 0,\n ADD COLUMN is_deleted TINYINT(1) DEFAULT 0\");\n} catch (PDOException $e) {\n // Abaikan jika kolom sudah ada\n}\n\n// Konfigurasi Folder & Pagination\n$folder = isset($_GET['folder']) ? $_GET['folder'] : 'inbox'; // inbox, starred, trash\n$limit = 20; \n$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;\nif ($page",
"Akun:",
"Loginkan 6 Digit PIN dari akun",
"/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n\n.pf-avatar {\n width: 64px; height: 64px; border-radius: 50%; overflow: hidden;\n border: 2px solid #e5e9f0; flex-shrink: 0;\n}\n.pf-avatar img { width: 100%; height: 100%; object-fit: cover; }\n\n.pf-soc-icon {\n width: 34px; height: 34px; border-radius: 50%; border: 1px solid #d4d4d8;\n display: inline-flex; align-items: center; justify-content: center;\n color: #333; font-size: 14px; text-decoration: none; transition: all .2s;\n background: #fff;\n}\n.pf-soc-icon:hover:not(.disabled) {\n background: #4B49AC; border-color: #4B49AC; color: #fff; text-decoration: none;\n}\n.pf-soc-icon.disabled {\n background: #f5f5f5; border-color: #e5e9f0; color: #ccc;\n cursor: not-allowed; pointer-events: none; opacity: 0.6;\n}\n\n.pf-edit-btn {\n font-size: 13px; font-weight: 500; color: #333; background: #fff;\n border: 1px solid #d4d4d8; border-radius: 6px; padding: 6px 16px;\n cursor: pointer; transition: all .15s; display: inline-flex; align-items: center;\n}\n.pf-edit-btn:hover { background: #f5f5f5; border-color: #bbb; }\n\n.pf-lbl {\n display: block; font-size: 12px; font-weight: 500; color: #4B49AC;\n margin-bottom: 4px;\n}\n.pf-val {\n font-size: 14px; font-weight: 400; color: #1a1a1a; margin-bottom: 0;\n}\n\n.pf-flbl {\n font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; display: block;\n}\n.pf-fb {\n font-size: 11px; margin-top: 3px; display: none; font-weight: 500;\n}\n.pf-fb.valid { display: block; color: #10b981; }\n.pf-fb.invalid { display: block; color: #ef4444; }\n\n/* Searchable Select */\n.ss-wrap { position: relative; }\n.ss-input { cursor: text; }\n.ss-input:disabled { background: #f8fafc; color: #aaa; cursor: not-allowed; }\n.ss-dropdown {\n display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 1060;\n background: #fff; border: 1px solid #e5e9f0; border-top: none; border-radius: 0 0 6px 6px;\n max-height: 200px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.08);\n}\n.ss-dropdown.show { display: block; }\n.ss-item {\n padding: 8px 14px; font-size: 13px; color: #333; cursor: pointer;\n border-bottom: 1px solid #f5f5f5; transition: background .1s;\n}\n.ss-item:last-child { border-bottom: none; }\n.ss-item:hover, .ss-item.active { background: #f0f4ff; color: #4B49AC; }\n.ss-empty { padding: 12px 14px; font-size: 12px; color: #aaa; text-align: center; }\n.ss-loading { padding: 12px 14px; font-size: 12px; color: #888; text-align: center; }\n.ss-loading::before {\n content: ''; display: inline-block; width: 14px; height: 14px;\n border: 2px solid #e5e9f0; border-top-color: #4B49AC; border-radius: 50%;\n animation: pfSpin .5s linear infinite; vertical-align: middle; margin-right: 6px;\n}\n@keyframes pfSpin { to { transform: rotate(360deg); } }",
")\"\n data-id=\"",
"Gagal Terhubung!",
"Jadwal kegiatan dan agenda mendatang,",
"Mencari wajah...",
"Pilih",
"Ambil Ulang",
"' + ssid);\n $('#saveStep4').fadeIn(300);\n \n // Perbarui kartu ESP32 di Dashboard\n $('#esp32-ssid').text(ssid);\n \n // Trigger Notification\n $.post('api_notifications.php', {\n action: 'create_notification',\n type: 'wifi_change',\n title: 'WiFi Terhubung',\n message: 'Berhasil berpindah ke WiFi: ' + ssid\n });\n }, 600);\n }\n }, 'json').fail(function() {\n // Jika API API PHP gak bisa diakses (misal server mati)\n if (elapsedWait >= maxTimeout) {\n clearInterval(changeCheckInterval);\n $('.save-step').hide();\n $('#saveStep2 .save-bar-fill').css({'width': '0%', 'transition': 'none'}); // Reset\n $('#saveErrorDisplay').text('Kesalahan koneksi ke server PHP API.');\n $('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');\n $('#saveStep5').fadeIn(300);\n }\n });\n }, 2500); // Check every 2.5 seconds\n } else {\n alert(\"Gagal menyimpan: \" + res.message);\n }\n }, 'json');\n });\n\n // Close fullscreen save modal and refresh on OK\n $('#btnCloseSaveModal').on('click', function() {\n $('#wifiSaveModal').modal('hide');\n // location.reload(); // Optional: reload page to ensure clean state\n });\n \n // Retry button on failed modal\n $('#btnRetrySaveModal').on('click', function() {\n $('#wifiSaveModal').modal('hide');\n });\n\n // SIMULATE WIFI DISCONNECT (Toggle locally for test, but actual logic relies on polling below)\n var isWifiConnected = true;\n $('#wifi-status-text').closest('p').css('cursor', 'pointer').on('click', function() {\n // Toggle for visual testing ONLY\n $(this).toggleClass('manual-override');\n });\n\n // ===== Real-time ESP32 Status Polling =====\n function pollESP32Status() {\n $.post('api_wifi.php', { action: 'get_status' }, function(res) {\n \n if ($('#wifi-status-text').closest('p').hasClass('manual-override')) return; // skip if manually toggled\n \n if(res.status === 'online') {\n if(!isWifiConnected || $('#wifi-status-text').text() !== 'WiFi Connected') {\n isWifiConnected = true;\n $('#wifi-status-text').text('WiFi Connected').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#10b981', 'box-shadow': 'none'});\n }\n \n // Update RSSI Signal Strength Icon\n var rssi = parseInt(res.rssi) || -90;\n var signalPct = Math.min(Math.max(2 * (rssi + 100), 0), 100); \n var sigIcon = signalPct > 70 ? 'mdi-wifi-strength-4' : (signalPct > 40 ? 'mdi-wifi-strength-2' : 'mdi-wifi-strength-1');\n var sigColor = signalPct > 70 ? '#10b981' : (signalPct > 40 ? '#f59e0b' : '#ef4444');\n $('#esp32-status-icon-container').html('",
"SIMPAN KATA SANDI",
"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!",
"kekuatan sinyal terlalu lemah",
"Berhasil dihapus.",
"Semua sistem terpantau disini !",
"di sebelah nama SSID untuk mencari WiFi di sekitar ESP32.",
"agar sistem dapat memverifikasi identitas Anda terlebih dahulu sebelum fitur ini dapat digunakan.\";\n } else {\n $text = \"Sesi chat Anda belum terhubung dengan Identia.\\n\\n👇",
"70) {\n lmFill.style.background = '#22c55e';\n lmIcon.style.color = '#22c55e';\n updateSecChip(secLive, true);\n } else if (livenessScore > 30) {\n lmFill.style.background = '#f59e0b';\n lmIcon.style.color = '#f59e0b';\n } else {\n lmFill.style.background = '#ef4444';\n lmIcon.style.color = '#ef4444';\n }\n }\n\n if (livenessScore >= 50) {\n setStatus('ok', 'Wajah terdeteksi — Siap!');\n btnShutter.disabled = false;\n } else {\n setStatus('search', 'Mendeteksi liveness... Tetap diam');\n btnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n setFaceNotOk();\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n updateSecChip(secFace, false);\n btnShutter.disabled = true;\n } else {\n setFaceNotOk();\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n updateSecChip(secFace, false);\n }\n }).catch(function(){});\n }, 300);\n }\n\n function setFaceNotOk() {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n btnShutter.disabled = true;\n }\n\n // ── Brightness Check ──\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
"// Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n\n // Username Verification Tooltip Logic\n const usernameInput = document.getElementById('usernameInput');\n const usernameTooltip = document.getElementById('usernameTooltip');\n const passwordInput = document.getElementById('loginPassword');\n const loginForm = document.getElementById('loginForm');\n let checkTimeout;\n\n function checkUsername() {\n const username = usernameInput.value.trim();\n if (username === '') {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity(''); \n passwordInput.disabled = false;\n return;\n }\n\n const formData = new FormData();\n formData.append('username', username);\n\n fetch('check_username.php', {\n method: 'POST',\n body: formData\n })\n .then(res => res.json())\n .then(data => {\n if (data.status === 'available') {\n // Available berarti belum terdaftar (error untuk login)\n usernameTooltip.style.display = 'flex';\n usernameInput.setCustomValidity('USERNAME TIDAK TERSEDIA');\n passwordInput.value = '';\n passwordInput.disabled = true;\n } else if (data.status === 'taken') {\n // Taken berarti username terdaftar di DB (valid untuk login)\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity('');\n passwordInput.disabled = false;\n }\n })\n .catch(err => console.error(err));\n }\n\n if (usernameInput) {\n usernameInput.addEventListener('input', function() {\n usernameTooltip.style.display = 'none';\n clearTimeout(checkTimeout);\n checkTimeout = setTimeout(checkUsername, 500); \n });\n usernameInput.addEventListener('blur', checkUsername);\n usernameInput.addEventListener('focus', function() {\n usernameTooltip.style.display = 'none';\n });\n }\n \n // --- High-Fidelity 3D Sphere Particle System (Antigravity Replica) ---",
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }",
"Your old password is shown below. Please enter and confirm your new password to update it.",
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-flex');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_occupant\").value = \"BULK\"; \n document.getElementById(\"delete_occupant_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteOccupant\").modal(\"show\"); \n });\n});\n\ndocument.getElementById(\"formTambahOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var btn = document.getElementById(\"btnSubmitOccupant\"); btn.disabled = true; btn.innerHTML = '",
"Konfirmasi password terbaru",
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'pin', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n if ($textMessage === '/forgot_password') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_forgot_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/forgot.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Verifikasi PIN 6-Digit', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Akses panel pemulihan telah disiapkan.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'edit', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n // --- Simpan Pesan Normal ke Admin ---\n // Account ID default biasanya di set ke 1\n $id_account = 1; \n \n // Cek apakah pesan ini adalah sebuah reply\n try { $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\"); } catch(PDOException $e) {}\n \n $replyToId = null;\n if (isset($update['message']['reply_to_message']) && isset($update['message']['reply_to_message']['message_id'])) {\n $tgReplyId = $update['message']['reply_to_message']['message_id'];\n // Temukan chat lokal berdasarkan telegram_msg_id\n $qRep = $pdo->prepare(\"SELECT id_chat FROM chats WHERE telegram_msg_id = ? LIMIT 1\");\n $qRep->execute([$tgReplyId]);\n $foundRep = $qRep->fetch();\n if ($foundRep) $replyToId = $foundRep['id_chat'];\n }\n \n $insertStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, is_read, telegram_msg_id, reply_to_chat_id) VALUES (?, ?, 'occupant', ?, 0, ?, ?)\");\n $insertStmt->execute([$id_account, $occupant['id_occupant'], $textMessage, $update['message']['message_id'], $replyToId]);\n \n // ALL PREVIOUS MESSAGES TO THIS USER ARE NOW READ\n try {\n $pdo->exec(\"UPDATE chats SET is_read = 1 WHERE id_occupant = {$occupant['id_occupant']} AND sender = 'account'\");\n } catch(PDOException $e) {}\n \n // Update last_active\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\"); } catch(PDOException $e) {}\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$occupant['id_occupant']]);\n \n } else {\n // Belum terhubung & memaksa untuk kirim kontak\n if (in_array($textMessage, ['/help', '/forgot_pin', '/forgot_password', '/edit_data'])) {\n $text = \"⚠️",
"\");\n}\n\n// Proses jika form disubmit via AJAX\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\n header('Content-Type: application/json');\n $name = trim($_POST['name']);\n $nik_nip = isset($_POST['nik_nip']) ? trim($_POST['nik_nip']) : '';\n \n if (empty($name)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Nama wajib diisi\"]);\n exit;\n }\n\n $fotoPath = $occupant['foto']; // Default bawaan\n if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {\n $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n $ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));\n\n if (!in_array($ext, $allowed)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Photo format not supported\"]);\n exit;\n }\n\n if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Max photo size is 5MB\"]);\n exit;\n }\n\n $uploadDir = __DIR__ . '/uploads/';\n if (!is_dir($uploadDir)) {\n mkdir($uploadDir, 0755, true);\n }\n\n $fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.' . $ext;\n $targetPath = $uploadDir . $fotoName;\n\n if (move_uploaded_file($_FILES['foto']['tmp_name'], $targetPath)) {\n $fotoPath = 'uploads/' . $fotoName;\n \n // Hapus foto lama jika ada dan bukan bawaan sistem\n if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {\n @unlink(__DIR__ . '/' . $occupant['foto']);\n }\n } else {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Failed mengunggah foto. Periksa izin folder.\"]);\n exit;\n }\n }\n\n // Handle base64 foto_data from camera capture\n $fotoData = isset($_POST['foto_data']) ? $_POST['foto_data'] : '';\n if (!empty($fotoData) && strpos($fotoData, 'data:image') === 0) {\n $uploadDir = __DIR__ . '/uploads/';\n if (!is_dir($uploadDir)) {\n mkdir($uploadDir, 0755, true);\n }\n\n // Decode base64\n $parts = explode(',', $fotoData, 2);\n if (count($parts) === 2) {\n $imgBinary = base64_decode($parts[1]);\n if ($imgBinary !== false) {\n $fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.jpg';\n $targetPath = $uploadDir . $fotoName;\n \n if (file_put_contents($targetPath, $imgBinary)) {\n // Hapus foto lama\n if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {\n @unlink(__DIR__ . '/' . $occupant['foto']);\n }\n $fotoPath = 'uploads/' . $fotoName;\n }\n }\n }\n }\n\n try {\n $stmtUpdate = $pdo->prepare(\"UPDATE occupant SET name = ?, nik_nip = ?, foto = ? WHERE id_occupant = ?\");\n $stmtUpdate->execute([$name, $nik_nip, $fotoPath, $id_occupant]);\n\n // Success, invalidate token dan update pesan telegram\n if (isset($tokenData['msg_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $chatId = '';\n $tstmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n @file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n \n // Send success message that auto-deletes after 5 seconds with live countdown\n $successLabel = \"✅ Data Success diperbarui\";\n $successText = \"",
"Memuat data...",
"Tautan ini tidak valid atau sudah digunakan.",
"Menghapus...');\n $.post('history_ajax.php', { action: 'hard_delete', ids: deleteIds }, function(res) {\n if(res.status === 'success') {\n $('#deleteHistorySuccessAlert').css('display', 'block');\n setTimeout(function() { window.location.reload(); }, 1200);\n } else {\n alert(res.message || 'Gagal menghapus');\n $('#btnDeleteHistoryConfirm').prop('disabled', false).html('Hapus');\n }\n }, 'json').fail(function() {\n alert('Koneksi gagal');\n $('#btnDeleteHistoryConfirm').prop('disabled', false).html('Hapus');\n });\n }\n});\n\n// Toggle Filter Inputs Display\nfunction toggleFilterInputs() {\n let type = document.getElementById('filterType').value;\n document.getElementById('filterDateRow').style.display = (type === 'date') ? 'block' : 'none';\n document.getElementById('filterMonthRow').style.display = (type === 'month') ? 'block' : 'none';\n document.getElementById('filterYearRow').style.display = (type === 'month' || type === 'year') ? 'block' : 'none';\n \n // Clear values when type changes\n if(type !== '",
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; /* Hapus garis horizontal bawah antar menu */ }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.tbl-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1.5px solid #eee; }\n.expand-arrow { font-size: 18px; color: #bbb; transition: transform 0.25s ease, color 0.2s; }\ntr.shown .expand-arrow { transform: rotate(180deg); color: #4b7bec; }\n.details-control:hover .expand-arrow { color: #4b7bec; }\n\n/* ══════════════ DROPDOWN — Seamless UI ══════════════ */\n#myTable tbody tr.shown > td {\n border-bottom: none !important;\n border-top: none !important; /* Hapus garis atas */\n}\n#myTable tbody tr.shown + tr > td {\n padding: 0 !important;\n margin: 0 !important;\n border-bottom: none !important; \n border-top: 1px solid #e2e8f0 !important; /* Tambah garis horizontal pemisah antar Menu dengan Halaman Dropdown */\n background-color: #fafafa !important;\n}\n#myTable tbody tr.shown + tr > td > div,\n#myTable tbody tr.shown + tr > td > * {\n padding: 0 !important;\n margin: 0 !important;\n}\n#myTable tbody tr.shown + tr > td:first-child {\n border-left: 1px solid #fafafa !important;\n border-bottom-left-radius: 8px;\n}\n#myTable tbody tr.shown + tr > td:last-child {\n border-right: 1px solid #fafafa !important;\n border-bottom-right-radius: 8px;\n}\n\n#myTable tbody tr.shown > td:first-child {\n border-left: 1px solid #e2e8f0 !important;\n border-top-left-radius: 8px;\n}\n#myTable tbody tr.shown > td:last-child {\n border-right: 1px solid #e2e8f0 !important;\n border-top-right-radius: 8px;\n}\n\n@keyframes ddSlideIn {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n.foto-wrapper { position: relative; display: inline-block; cursor: pointer; width: 72px; height: 72px; }\n.foto-circle { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; border: 2px solid #e0e0e0; transition: border-color 0.2s; }\n.foto-badge { position: absolute; bottom: 0; right: 0; width: 24px; height: 24px; border-radius: 50%; background: #4b7bec; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }\n.foto-badge i { color: #fff; font-size: 12px; }\n.foto-wrapper:hover .foto-circle { border-color: #4b7bec; }\n.btn-custom-edit {\n background: #e2e6ea;\n border: none;\n color: #212529;\n transition: background 0.2s ease;\n}\n.btn-custom-edit:hover, .btn-custom-edit:focus {\n background: #cbd3da;\n color: #212529;\n}\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.code-big { font-size: 26px; letter-spacing: 8px; color: #222; font-weight: 700; border-right: none; }\n.code-input-group .form-control { border-right: none; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n.countdown-timer { font-weight: 700; color: #dc2626; font-size: 12px; margin-left: 8px; display: inline-flex; align-items: center; background: #fef2f2; padding: 3px 10px; border-radius: 6px; border: 1px solid #fecaca; white-space: nowrap; }\n@keyframes pulse-timer { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }",
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'forgot', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n if ($textMessage === '/edit_data') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_edit_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/edit_data.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Perbarui Data Profil', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Sistem Pembaruan Data Identitas telah disediakan untuk Anda.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
"Confirm Password",
"Foto Belum Diverifikasi",
"Dilindungi dengan skema watermark digital",
"\"\n data-hasfoto=\"",
"Gagal memuat QR",
"Memverifikasi...';\n btn.disabled = true;\n \n $.post(\"verify_pin_account.php\", { id_account: id, pin: pin }, function(d) {\n btn.innerHTML = '",
"Nama lengkap",
"\" alt=\"foto\" class=\"ml-2\" style=\"width: 24px; height: 24px; object-fit: cover; border-radius: 4px; border: 1px solid #ddd;\">",
"Batal",
"`;}\n if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;\n h+=`",
"Menghubungkan ke jaringan baru...",
"Menyimpan...';\n btn.disabled = true;\n\n const formData = new FormData(this);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n document.getElementById('step2-password').style.display = 'none';\n document.getElementById('step3-success').style.display = 'block';\n \n if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.HapticFeedback) {\n window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');\n }\n\n // Auto close after 10 seconds\n let count = 10;\n const cText = document.getElementById('countdownText');\n const cInterval = setInterval(() => {\n count--;\n cText.innerText = count;\n if(count",
"`;}\n if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';\n h+=`",
"Kelola data akun operasional,",
".pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus {\n border-bottom-color: #4B49AC;\n }\n\n .custom-alert {\n position: fixed;\n top: 20px;\n right: 20px;\n background: #fff;\n border-left: 4px solid #fcd34d;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px;\n border-radius: 4px;\n z-index: 9999;\n display: none;\n align-items: center;\n gap: 12px;\n min-width: 300px;\n max-width: 400px;\n animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n @keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n\n /* Style for the custom tooltip to look native */\n .custom-tooltip-wrapper {\n position: relative;\n display: block;\n }\n .custom-tooltip {\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n background-color: white;\n color: #333;\n padding: 8px 15px;\n border-radius: 4px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n font-size: 14px;\n white-space: nowrap;\n z-index: 1000;\n margin-top: 8px;\n border: 1px solid #ddd;\n display: none; /* hidden by default */\n align-items: center;\n gap: 8px;\n }\n .custom-tooltip::before {\n content: '';\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n border-width: 8px;\n border-style: solid;\n border-color: transparent transparent #ddd transparent;\n }\n .custom-tooltip::after {\n content: '';\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n border-width: 7px;\n border-style: solid;\n border-color: transparent transparent white transparent;\n }\n .custom-tooltip .error-icon {\n background-color: #ff9800;\n color: white;\n width: 20px;\n height: 20px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 3px;\n font-weight: bold;\n font-family: serif;\n }\n\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; margin-bottom: 1rem; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n \n body, .container-scroller, .page-body-wrapper, .content-wrapper {\n background-color: #f8f9fa !important;\n }",
"Verifikasi Identitas",
"— pastikan tidak ada kesalahan ketik, perhatikan huruf besar/kecil (case-sensitive), spasi, dan karakter khusus seperti @, #, !, dsb.',\n '",
"Nama Kegiatan",
"Sentuh sensor pada perangkat.",
"ESP32 juga gagal kembali ke WiFi sebelumnya. Saat ini ESP32 memancarkan hotspot konfigurasi. Hubungkan ke WiFi",
"Sidik Jari Berhasil Disimpan!",
"Your profile data has been updated.",
"karena",
"Ulangi Password",
"alasan yang tidak dapat diidentifikasi secara spesifik",
"Password dan Confirm Password tidak sama!",
"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
"Email address",
"ingin memperbarui foto, klik",
"`;}\n el('gInd').innerHTML=h;\n}\nfunction uInd(){\n const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');\n if(tf){const e=tf.extra_data||{};el('info-tf').innerHTML=mr('mdi-page-layout-body','Page',e.current_page||'Standby','b')+mr('mdi-brightness-6','Brightness',(e.brightness||100)+'%','c');el('ft-tf').innerHTML='",
"';\n data.occupant.forEach(item => {\n const sub = item.no_hp ? item.no_hp : 'No phone';\n html += `",
"Menyimpan pengaturan ESP32...",
"Berhasil!",
"Lihat semua jadwal disini !",
"Foto diambil via QR code setelah data tersimpan",
"ESP32 gagal terhubung ke jaringan",
"Preview Foto",
"Menyimpan Sidik Jari...';\n \n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.style.display = \"none\";\n successWrapper.style.display = \"block\";\n btn.style.display = \"none\";\n } else {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"// Init Telegram WebApp\n window.Telegram.WebApp.ready();\n window.Telegram.WebApp.expand();\n\n let customAlertTimeout;\n let closeAlertTimeout;\n\n function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n clearTimeout(customAlertTimeout);\n clearTimeout(closeAlertTimeout);\n\n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n customAlertTimeout = setTimeout(() => closeCustomAlert(), 5000);\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n clearTimeout(customAlertTimeout);\n clearTimeout(closeAlertTimeout);\n closeAlertTimeout = setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n // Logic Input PIN (mirip di forgot_password.php)\n const pinInputs = document.querySelectorAll('.pin-box');\n \n pinInputs.forEach((input, index) => {\n input.addEventListener('input', function(e) {\n // Allow only numbers\n this.value = this.value.replace(/[^0-9]/g, '');\n \n if (this.value !== '') { // Jika diisi\n this.classList.remove('input-error');\n if (index",
"Koneksi Dipulihkan:",
"Memverifikasi Data...",
"Input ulang password dengan benar",
"t.stop());\n editCamArea.style.display = 'none';\n editPreviewWrap.style.display = 'block';\n document.querySelector('.ecam-hud').style.display = 'none';\n editBarAfter.style.display = 'flex';\n }\n\n function editRetake() {\n editDataURL = null;\n editLivenessScore = 0; editLivenessFrames = 0; editPrevLandmarks = null;\n editLmFill.style.height = '0%';\n editPreviewWrap.style.display = 'none';\n editCamArea.style.display = 'block';\n editBarAfter.style.display = 'none';\n document.querySelector('.ecam-hud').style.display = 'flex';\n editCamLoading.style.display = 'flex';\n editLoadText.textContent = 'Memuat ulang kamera...';\n editSetProgress(50);\n initEditCamera();\n }\n\n function editUseFoto() {\n if (!editDataURL) return;\n document.getElementById('previewFoto').src = editDataURL;\n document.getElementById('fotoData').value = editDataURL;\n stopEditCamera();\n showCustomAlert('Success', 'Foto berhasil diambil. Klik Save Data untuk menyimpan.', 'success');\n }\n\n function stopEditCamera() {\n document.getElementById('cameraOverlay').style.display = 'none';\n clearInterval(editDetectLoop);\n clearInterval(editBrightInterval);\n if (editStream) { editStream.getTracks().forEach(t => t.stop()); editStream = null; }\n }\n\n editBtnShutter.addEventListener('click', editCapture);\n document.getElementById('editBtnRetake').addEventListener('click', editRetake);\n document.getElementById('editBtnUseFoto').addEventListener('click', editUseFoto);\n\n document.getElementById('btnSimpan').addEventListener('click', function() {\n // Validate step 1 data again just in case\n const name = document.getElementById('name').value.trim();\n if(name === '') {\n showCustomAlert('Incomplete Form', 'Full Name wajib diisi.', 'warning');\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step1-data').style.display = 'block';\n return;\n }\n\n const btn = document.getElementById('btnSimpan');\n const originalText = btn.innerHTML;\n btn.innerHTML = '",
"Selesai",
"Gagal memindai jaringan (Timeout). Pastikan ESP32 menyala.",
"';\n loading.style.display = 'block';\n });\n}\n\nfunction copyQrFotoLink() {\n navigator.clipboard.writeText(document.getElementById('qrFotoLinkText').value);\n showCustomAlert('Link berhasil disalin!');\n}\n\nfunction downloadQrFoto() {\n var qrCanvas = document.querySelector('#qrFotoCanvas canvas');\n if (!qrCanvas) { alert(\"QR Code belum ter-generate.\"); return; }\n \n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Edit_Foto.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}",
"Loginkan kata sandi baru untuk akun Anda",
"— jauhkan ESP32 dan router dari perangkat elektronik yang menghasilkan interferensi seperti microwave, telepon cordless, atau perangkat Bluetooth yang aktif.'\n ];\n } else if (reason.indexOf('timeout') !== -1 || elapsedWait >= maxTimeout) {\n errMsg = 'Waktu Tunggu Habis (Connection Timeout)';\n reasonText = '",
"\");\n}\n\n// Proses jika form disubmit via AJAX\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\n header('Content-Type: application/json');\n $action = isset($_POST['action']) ? $_POST['action'] : '';\n\n if ($action === 'verify_pin') {\n $pin = isset($_POST['pin']) ? trim($_POST['pin']) : '';\n if (empty($pin)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"PIN wajib diisi.\"]);\n exit;\n }\n\n $stmt = $pdo->prepare(\"SELECT pin FROM occupant WHERE id_occupant = ?\");\n $stmt->execute([$id_occupant]);\n $data = $stmt->fetch(PDO::FETCH_ASSOC);\n\n if ($data && $data['pin'] === $pin) {\n echo json_encode([\"status\" => \"success\", \"message\" => \"PIN Valid. Silakan masukkan kata sandi baru.\"]);\n } else {\n echo json_encode([\"status\" => \"error\", \"message\" => \"PIN tidak valid!\"]);\n }\n exit;\n }\n\n if ($action === 'reset_password') {\n $password = isset($_POST['password']) ? $_POST['password'] : '';\n $confirm_password = isset($_POST['confirm_password']) ? $_POST['confirm_password'] : '';\n\n if (empty($password) || empty($confirm_password)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Semua kolom wajib diisi.\"]);\n exit;\n }\n if ($password !== $confirm_password) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Passwords do not match.\"]);\n exit;\n }\n\n // Add password columns to occupant if they don't exist yet\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN password VARCHAR(255) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN password_enc VARCHAR(255) NULL AFTER password\"); } catch(PDOException $e) {}\n\n $password_hash = password_hash($password, PASSWORD_DEFAULT);\n\n try {\n $stmt = $pdo->prepare(\"UPDATE occupant SET password = ?, password_enc = ? WHERE id_occupant = ?\");\n if ($stmt->execute([$password_hash, $password, $id_occupant])) {\n \n // Edit Telegram Message to disable the inline button\n if (isset($tokenData['msg_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $chatId = $occupant['telegram_id'] ?? $tokenData['id_occupant'];\n // We need actual telegram_id. \n $tstmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n @file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n \n // Send success message that auto-deletes after 5 seconds with live countdown\n $successLabel = \"✅ Data Success di simpan\";\n $successText = \"",
"Forgot Password",
"' + (d.message || 'Gagal generate QR') + '",
"Create a 6-digit PIN for additional security.",
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n } else {\n showToast('err', d.message || 'Gagal menyimpan');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
"Ketik provinsi lalu pilih...",
"function copyFingerQrLink() {\n var v = document.getElementById(\"fingerQrLinkText\").value;\n navigator.clipboard.writeText(v).then(function(){ showCustomAlert('Link berhasil disalin!'); });\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Draw Background\n ctx.fillStyle = '#f8fafc';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Add Decorative Top Gradient Bar\n var gradient = ctx.createLinearGradient(0, 0, width, 0);\n gradient.addColorStop(0, '#4b49ac');\n gradient.addColorStop(1, '#94a3b8');\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, width, 15);\n \n // 3. Draw Header Details\n ctx.fillStyle = '#1e293b';\n ctx.font = 'bold 36px Arial, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('IDENTITAS DIGITAL', width / 2, 80);\n \n ctx.fillStyle = '#64748b';\n ctx.font = '22px Arial, sans-serif';\n ctx.fillText('Akses Cepat & Aman', width / 2, 120);\n \n // 4. White Box Container\n var boxWidth = 600;\n var boxHeight = 700;\n var boxX = (width - boxWidth) / 2;\n var boxY = 160;\n \n ctx.fillStyle = '#ffffff';\n ctx.shadowColor = 'rgba(0, 0, 0, 0.08)';\n ctx.shadowBlur = 30;\n ctx.shadowOffsetY = 15;\n ctx.beginPath();\n ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 20);\n ctx.fill();\n ctx.shadowColor = 'transparent';\n \n // 5. Draw QR Code centered inside the container\n var qrSize = 460;\n var x = (width - qrSize) / 2;\n var y = boxY + 80;\n ctx.drawImage(qrCanvas, x, y, qrSize, qrSize);\n \n // Border around QR Code\n ctx.strokeStyle = '#e2e8f0';\n ctx.lineWidth = 4;\n ctx.strokeRect(x - 10, y - 10, qrSize + 20, qrSize + 20);\n \n ctx.fillStyle = '#334155';\n ctx.font = 'bold 24px Arial, sans-serif';\n ctx.fillText('Scan untuk Mendaftar Sidik Jari', width / 2, y - 25);\n \n // 6. Add Footer Note\n ctx.fillStyle = '#1e293b';\n ctx.font = 'bold 26px Arial, sans-serif';\n ctx.fillText('SKY DASH APP', width / 2, y + qrSize + 70);\n \n ctx.fillStyle = '#64748b';\n ctx.font = '20px Arial, sans-serif';\n ctx.fillText('Powered by Identity System', width / 2, y + qrSize + 105);\n \n return composite.toDataURL(\"image/jpeg\", 1.0);\n}\n\nfunction downloadFingerQr() {\n var qrCanvas = document.querySelector('#fingerprintQrCanvas canvas');\n if (!qrCanvas) { alert(\"QR Code belum ter-generate.\"); return; }\n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Fingerprint_Account.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}\n\nfunction showFingerprintModal(accountId) {\n document.getElementById(\"fingerprintAccountId\").value = accountId;\n $(\"#modalFingerprint\").modal(\"show\");\n \n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'block';\n document.getElementById(\"fingerprintQrView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'none';\n \n fetch(\"generate_finger_token_account.php?id=\" + accountId)\n .then(r => r.json())\n .then(d => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n if(d.status === 'success') {\n document.getElementById(\"fingerprintQrView\").style.display = 'block';\n document.getElementById(\"fingerQrLinkText\").value = d.url;\n var canvas = document.getElementById(\"fingerprintQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url, width: 150, height: 150,\n colorDark: '#0f172a', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n }\n }).catch(e => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n });\n}\n\nasync function startWebAuthnRegistration() {\n var btn = document.getElementById(\"btnScanFingerHere\");\n var statusEl = document.getElementById(\"webAuthnStatus\");\n var errorEl = document.getElementById(\"webAuthnError\");\n var successWrapper = document.getElementById(\"webAuthnStatusWrapper\");\n var accountId = document.getElementById(\"fingerprintAccountId\").value;\n \n btn.disabled = true;\n errorEl.style.display = \"none\";\n successWrapper.style.display = \"none\";\n \n statusEl.innerHTML = '",
"Hapus Account?",
"Tekan untuk menyimpan perubahan",
"');\n $('#wifiNetworkList').fadeIn(300);\n });\n });\n\n // Click on network to fill SSID\n $(document).on('click', '.wifi-network-item', function() {\n var ssid = $(this).data('ssid');\n $('#new-ssid').val(ssid);\n $('#wifiScanModal').modal('hide');\n $('#new-wifi-pass').val('').focus();\n });\n\n // Hover effect on network items\n $(document).on('mouseenter', '.wifi-network-item', function() {\n $(this).css('background', '#f0eeff');\n }).on('mouseleave', '.wifi-network-item', function() {\n $(this).css('background', '');\n });\n\n // ===== WIFI SAVE WITH FULLSCREEN ANIMATION =====\n $('#saveWifiBtn, #wifiSaveModal .btn:last-child').on('click', function() {\n var ssid = $('#new-ssid').first().val().trim();\n var pass = $('#new-wifi-pass').first().val().trim();\n if (!ssid) { alert('SSID cannot be empty'); return; }\n\n // Send change API command\n $.post('api_wifi.php', { action: 'change_wifi', ssid: ssid, password: pass }, function(res) {\n if(res.success) {\n // Reset all steps\n $('.save-step').hide();\n $('.save-bar-fill').css('width', '0%');\n // Reset modal to small for non-failure steps\n $('#wifiSaveModalDialog').removeClass('modal-lg').addClass('modal-sm');\n $('#saveStep1').show();\n $('#wifiSaveModal').modal('show');\n\n // Step 1: Connecting (0-2.5s)\n setTimeout(function() { $('#saveStep1 .save-bar-fill').css('width', '50%'); }, 500);\n setTimeout(function() { $('#saveStep1 .save-bar-fill').css('width', '100%'); }, 1800);\n\n // Step 2 & 3: Waiting logic\n setTimeout(function() {\n $('.save-step').hide();\n $('#saveStep2').fadeIn(300);\n \n // Animasi progress bar step 2 melambat (pura-pura proses hingga batas waktu)\n $('#saveStep2 .save-bar-fill').css({'width': '85%', 'transition': 'width 30s cubic-bezier(0.1, 0.8, 0.3, 1)'});\n }, 1800);\n\n // Simpan sesi ganti WiFi & Mulai Timer Timeout (Misal 60 Detik maksimal)\n var isCheckingChangeStatus = true;\n var maxTimeout = 90; // detik\n var elapsedWait = 0;\n \n // Polling status penggantian WiFi\n var changeCheckInterval = setInterval(function() {\n elapsedWait += 2.5; \n \n $.get('api_wifi.php?action=check_change_status', function(statusRes) {\n if (statusRes.status === 'failed' || elapsedWait >= maxTimeout) {\n clearInterval(changeCheckInterval);\n var reason = statusRes.reason || '';\n var errMsg = statusRes.status === 'failed' ? (statusRes.message || 'Gagal terhubung.') : 'Waktu Tunggu Habis (Timeout).';\n \n // Build detailed failure explanation\n var reasonText = '';\n var suggestions = [];\n var fallbackHtml = '';\n var targetSsid = $('#new-ssid').first().val().trim();\n \n if (reason.indexOf('wrong_password') !== -1) {\n errMsg = 'Autentikasi WiFi Ditolak (Wrong Password)';\n reasonText = '",
"Menghubungkan ke server.",
"';\n }\n };\n xhr.onerror = function() {\n showToast('err', 'Koneksi gagal');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
"Hubungi occupant Anda melalui Integrasi Telegram Bot.",
"Verify your identity to reset password.",
"`;\n });\n }\n\n // Occupant\n if (showOccupant && data.occupant && data.occupant.length > 0) {\n html += '",
".pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus {\n border-bottom-color: #4B49AC;\n }\n .text-separator {\n text-align: center;\n color: #888;\n font-size: 14px;\n margin: 15px 0;\n }\n \n .custom-alert {\n position: fixed;\n top: 20px;\n right: 20px;\n background: #fff;\n border-left: 4px solid #fcd34d;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px;\n border-radius: 4px;\n z-index: 9999;\n display: none;\n align-items: center;\n gap: 12px;\n min-width: 300px;\n max-width: 400px;\n animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n @keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n \n body, .container-scroller, .page-body-wrapper, .content-wrapper {\n background-color: #f8f9fa !important;\n }",
"Setup PIN Gagal",
"showCustomAlert(\"Pendaftaran Gagal\", \"",
"Gunakan menu ini untuk Memindahkan ESP32 ke koneksi WiFi lain (Misal dari WiFi A ke WiFi B). Tekan tombol",
"';\n data.overview.forEach(item => {\n html += `",
"Klik foto di atas untuk mengubah via QR Code",
"Daftarkan akun baru,",
"7) {\n let jam = actDate.getHours().toString().padStart(2, '0');\n let menit = actDate.getMinutes().toString().padStart(2, '0');\n let tgl = actDate.getDate().toString().padStart(2, '0');\n let bln = (actDate.getMonth() + 1).toString().padStart(2, '0');\n let thn = actDate.getFullYear();\n occStatStr = `Terakhir Online ${jam}.${menit} ${tgl}-${bln}-${thn}`;\n } else {\n const hariArr = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];\n let namaHari = hariArr[actDate.getDay()];\n let jam = actDate.getHours().toString().padStart(2, '0');\n let menit = actDate.getMinutes().toString().padStart(2, '0');\n occStatStr = `Terakhir Online Jam ${jam}:${menit} Hari ${namaHari}`;\n }\n }\n document.getElementById('activeStatus').innerText = occStatStr;\n document.getElementById('activeAvatar').src = res.occupant.foto;\n activeAvatarUrl = res.occupant.foto;\n \n renderMessages(res.data);\n loadContacts();\n }\n }, 'json');\n}\n\nfunction backToContacts() {\n document.querySelector('.chat-wrapper').classList.remove('mobile-chat-open');\n activeOccupantId = null;\n document.getElementById('chatBlank').style.display = 'flex';\n document.getElementById('chatActive').style.display = 'none';\n renderContacts(contacts);\n}\n\nfunction buildMessagesHtml(messages) {\n let html = '';\n const adminAvatar = 'images/admin.png';\n \n messages.forEach(msg => {\n let isRight = (msg.sender === 'account');\n let cls = isRight ? 'msg-right' : 'msg-left';\n \n let statusHtml = '';\n if (isRight) {\n if (msg.is_read == 1) {\n statusHtml = '",
"Autentikasi biometrik diperlukan untuk membuka akses pintu IoT.",
"0.6) { editSetFaceNotOk(); editSetStatus('err', 'Wajah terlalu dekat'); }\n else if (dx > 0.2 || dy > 0.2) { editSetFaceNotOk(); editSetStatus('err', 'Pusatkan wajah'); }\n else {\n editFaceOk = true;\n editOval.classList.add('ok');\n if (landmarks) {\n const pts = landmarks.positions;\n if (editPrevLandmarks && pts.length === editPrevLandmarks.length) {\n let totalDiff = 0;\n for (let i = 0; i",
"/edit_data",
"Pindah ke Sampah",
"Kesalahan koneksi ke server.",
"Lengkapi profil sekarang!",
"Instagram belum diisi",
"const pwForm = document.getElementById('passwordForm');\n if(pwForm) {\n pwForm.addEventListener('submit', function(e) {\n var pass = document.getElementById(\"newPassword\").value;\n const pwValid = (pass.length >= 8 && /[a-z]/.test(pass) && /[A-Z]/.test(pass) && /\\d/.test(pass) && /[^A-Za-z0-9]/.test(pass));\n if (!pwValid) {\n e.preventDefault();\n showCustomAlert(\"Warning\", \"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!\", \"warning\");\n document.getElementById(\"newPassword\").focus();\n }\n });\n\n // Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n\n // Password Strength Logic\n document.getElementById('newPassword').addEventListener('input', function() {\n var pass = this.value;\n var meter = document.getElementById('pwStrengthMeterReg');\n var bar = document.getElementById('pwStrengthBarReg');\n var text = document.getElementById('pwStrengthTextReg');\n\n if(pass.length > 0) {\n meter.style.display = 'flex';\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.style.width = '100%'; bar.style.backgroundColor = '#10b981'; // Kuat\n text.innerText = 'Strong Password'; text.style.color = '#10b981';\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.style.width = '66%'; bar.style.backgroundColor = '#f59e0b'; // Sedang\n text.innerText = 'Medium Password'; text.style.color = '#f59e0b';\n } else {\n bar.style.width = '33%'; bar.style.backgroundColor = '#ef4444'; // Lemah\n text.innerText = 'Weak Password'; text.style.color = '#ef4444';\n }\n } else {\n meter.style.display = 'none';\n text.innerText = '';\n }\n });\n }",
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n fd.append('liveness_score', Math.round(livenessScore));\n fd.append('capture_time', new Date().toISOString());\n fd.append('device_info', navigator.userAgent);\n fd.append('screen_res', screen.width + 'x' + screen.height);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n sessionSaved = true;\n clearInterval(tokenCheckInterval);\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n // Auto-close countdown\n var count = 30;\n var cdEl = document.getElementById('sucCountdown');\n var cdInterval = setInterval(function() {\n count--;\n if (cdEl) cdEl.textContent = 'Halaman otomatis tertutup dalam ' + count + ' detik';\n if (count",
"// Tambah\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
"') {\n document.getElementById('filterDateRow').value = '';\n document.getElementById('filterMonthRow').value = '';\n document.getElementById('filterYearRow').value = '';\n }\n checkFilterForm();\n}\n\nfunction checkFilterForm() {\n let type = document.getElementById('filterType').value;\n let isValid = false;\n \n if (type === 'date') {\n isValid = document.getElementById('filterDateRow').value.trim() !== '';\n } else if (type === 'month') {\n isValid = document.getElementById('filterMonthRow').value !== '' && document.getElementById('filterYearRow').value.trim() !== '';\n } else if (type === 'year') {\n isValid = document.getElementById('filterYearRow').value.trim() !== '';\n }\n \n document.getElementById('btnSubmitFilter').disabled = !isValid;\n}\n\n// Run validation on load based on current inputs\n$(document).ready(function() {\n checkFilterForm();\n});\n\n// Live AJAX Search\nlet searchTimer;\n$('#searchInput').on('keyup', function() {\n clearTimeout(searchTimer);\n let form = $(this).closest('form');\n // Set page to 1 on new search\n let formData = form.serializeArray();\n let queryParams = [];\n $.each(formData, function(i, field) {\n queryParams.push(field.name + '=' + encodeURIComponent(field.value));\n });\n queryParams.push('page=1');\n \n let url = form.attr('action') + '?' + queryParams.join('&');\n \n searchTimer = setTimeout(function() {\n // Update browser URL without reload\n window.history.pushState({}, '', url);\n \n // Show lightweight loading indication\n $('#emailListWrapper').css('opacity', '0.5');\n \n // Fetch new data\n $.get(url, function(data) {\n let $parsed = $(data);\n $('#emailListWrapper').html($parsed.find('#emailListWrapper').html()).css('opacity', '1');\n $('#paginationControls').html($parsed.find('#paginationControls').html());\n });\n }, 150); // 150ms debounce for faster feel\n});\n\n// Prevent traditional form submission on enter\n$('#searchForm').on('submit', function(e) {\n e.preventDefault();\n});",
"\"\n data-fingerid=\"",
". Berikut adalah PIN rahasia Anda:",
"Periksa firewall dan isolasi klien",
"Belum ada kegiatan",
"';\n data.account.forEach(item => {\n html += `",
"70?'er':sc(sd.status)}\" id=\"bdg-sd\">${tmp>70?'OVERHEAT':(e.state==='active'?'ACTIVE':'INACTIVE')}",
"oleh firmware mikrokontroler.' ;\n suggestions = [\n '",
"Foto Profil",
"';\n\n fetch('search_api.php?q=' + encodeURIComponent(query))\n .then(r => r.json())\n .then(res => {\n if (res.status === 'success') {\n renderResults(res.data, query, activeTab);\n }\n })\n .catch(err => {\n searchResults.innerHTML = '",
"Catatan Keamanan: Semua antarmuka di atas dikelola melalui sesi Telegram Web App yang terenkripsi dan otomatis kedaluwarsa setelah 5 menit demi menjamin privasi Anda.",
"Gagal",
"= 3) {\n clearInterval(detectionLoopInterval);\n clearInterval(brightInterval);\n stream.getTracks().forEach(t => t.stop());\n \n pgCamera.classList.add('page-hidden');\n pgSuccess.classList.remove('page-hidden');\n\n // Notify Server to invalidate token and edit TG message\n let fd = new FormData();\n fd.append('action', 'success');\n fd.append('token', '",
"`).join('');\n}\nfunction insertEmoji(e) {\n let input = document.getElementById('messageInput');\n input.value += e;\n input.style.height = 'auto';\n input.style.height = (input.scrollHeight) + 'px';\n input.focus();\n}\n\ndocument.addEventListener('click', function(e) {\n if(e.target.classList.contains('emoji-tab')) {\n document.querySelectorAll('.emoji-tab').forEach(t => t.classList.remove('active'));\n e.target.classList.add('active');\n renderEmojis(e.target.dataset.cat);\n }\n \n let picker = document.getElementById('emojiPicker');\n let btn = document.getElementById('btnEmoji');\n if (picker && btn && picker.classList.contains('show') && !picker.contains(e.target) && !btn.contains(e.target)) {\n picker.classList.remove('show');\n }\n});\n\n\n// Context Menu Logic\nlet selectedMsgId = null;\nlet selectedMsgSender = null;\nlet selectedMsgText = null;\nlet replyingToId = null;\nlet editingMsgId = null;\nlet originalEditMsgText = null;\n\n// Selection Mode State\nlet isSelectionMode = false;\nlet selectedMessages = []; // Array of objects: {id, sender}\n\nfunction showContextMenu(e, idChat, sender, bubbleEl) {\n e.preventDefault();\n selectedMsgId = idChat;\n selectedMsgSender = sender;\n \n // Extract text ignoring the reply block and the (Diedit) tag\n let clone = bubbleEl.cloneNode(true);\n let replyBlocks = clone.querySelectorAll('.msg-reply-block');\n replyBlocks.forEach(b => b.remove());\n let editTags = clone.querySelectorAll('i'); // Removes the (Diedit) tag along with other icons\n editTags.forEach(t => t.remove());\n selectedMsgText = (clone.textContent || '').trim();\n \n let menu = document.getElementById('msgContextMenu');\n \n // Show/hide Edit button based on sender\n let btnEdit = document.getElementById('ctxMenuEdit');\n if (sender === 'account') {\n btnEdit.style.display = 'flex';\n } else {\n btnEdit.style.display = 'none';\n }\n \n let x = e.clientX;\n let y = e.clientY;\n if (x + 180 > window.innerWidth) x = window.innerWidth - 190;\n if (y + 130 > window.innerHeight) y = window.innerHeight - 130;\n menu.style.left = x + 'px';\n menu.style.top = y + 'px';\n menu.style.display = 'block';\n}\n\nfunction prepareReply(event) {\n if(event) event.stopPropagation();\n replyingToId = selectedMsgId;\n let title = (selectedMsgSender === 'account') ? 'Membalas Anda' : ('Membalas ' + document.getElementById('activeName').innerText);\n document.getElementById('rpTitle').innerText = title;\n document.getElementById('rpText').innerText = selectedMsgText;\n document.getElementById('replyPreview').style.display = 'block';\n document.getElementById('messageInput').focus();\n hideContextMenu();\n}\n\nfunction closeReplyBox() {\n replyingToId = null;\n document.getElementById('replyPreview').style.display = 'none';\n}\n\nfunction hideContextMenu() {\n document.getElementById('msgContextMenu').style.display = 'none';\n}\n\ndocument.addEventListener('click', function(e) {\n hideContextMenu();\n});\n\nfunction deleteSelectedMessage(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n if (!selectedMsgId) return;\n \n let msgElement = document.getElementById('msg-' + selectedMsgId);\n \n if (selectedMsgSender === 'account') {\n // Pesan Admin: Ada pilihan Hapus untuk Semua atau Hanya Saya\n Swal.fire({\n title: 'Hapus Pesan?',\n text: \"Pilih metode penghapusan.\",\n icon: 'warning',\n showDenyButton: true,\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Semua',\n denyButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-all',\n denyButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n // Semua\n executeDelete(selectedMsgId, msgElement, 'everyone');\n } else if (result.isDenied) {\n // Saya\n executeDelete(selectedMsgId, msgElement, 'me');\n }\n });\n } else {\n // Pesan Occupant: Hanya bisa Hapus untuk Saya\n Swal.fire({\n title: 'Hapus Pesan?',\n text: \"Pesan ini hanya terhapus pada layar Anda.\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeDelete(selectedMsgId, msgElement, 'me');\n }\n });\n }\n}\n\nfunction executeDelete(id, msgElement, type) {\n if (msgElement) msgElement.style.opacity = '0.5';\n $.post('chat_api.php', { action: 'delete_message', id_chat: id, delete_type: type }, function(res) {\n if(res.status === 'success') {\n $('#msg-' + id).fadeOut(300, function() { $(this).remove(); });\n } else {\n if (msgElement) msgElement.style.opacity = '1';\n Swal.fire('Gagal', res.message || 'Pesan tidak dapat dihapus', 'error');\n }\n }, 'json');\n}\n\n// ========================\n// BULK DELETE & SELECTION\n// ========================\n\nfunction enterSelectionMode(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n \n isSelectionMode = true;\n selectedMessages = [{ id: selectedMsgId, sender: selectedMsgSender }];\n \n // Stop editing/replying if active\n cancelEdit();\n closeReplyBox();\n \n // Switch header actions\n document.getElementById('normalHeaderActions').style.display = 'none';\n document.getElementById('selectionHeaderActions').style.display = 'flex';\n document.getElementById('selectionCount').innerText = selectedMessages.length;\n \n // Add selectable classes to all rows and selected to the first one\n let rows = document.querySelectorAll('.msg-row');\n rows.forEach(row => {\n row.classList.add('selectable');\n // Add onclick to rows that are currently loaded\n row.setAttribute('onclick', `toggleSelectMessage(event, ${row.getAttribute('data-id')}, '${row.classList.contains('msg-right') ? 'account' : 'occupant'}')`);\n });\n document.getElementById('msg-' + selectedMsgId).classList.add('selected');\n}\n\nfunction exitSelectionMode() {\n isSelectionMode = false;\n selectedMessages = [];\n \n // Switch header actions back\n document.getElementById('normalHeaderActions').style.display = 'block';\n document.getElementById('selectionHeaderActions').style.display = 'none';\n \n // Remove selectable classes\n let rows = document.querySelectorAll('.msg-row');\n rows.forEach(row => {\n row.classList.remove('selectable', 'selected');\n row.removeAttribute('onclick'); // Important: remove the bulk select onclick\n });\n}\n\nfunction toggleSelectMessage(event, idChat, sender) {\n if(event) event.stopPropagation();\n if(!isSelectionMode) return;\n \n let index = selectedMessages.findIndex(m => m.id === idChat);\n let row = document.getElementById('msg-' + idChat);\n \n if (index > -1) {\n // Deselect\n selectedMessages.splice(index, 1);\n if(row) row.classList.remove('selected');\n } else {\n // Select\n selectedMessages.push({ id: idChat, sender: sender });\n if(row) row.classList.add('selected');\n }\n \n document.getElementById('selectionCount').innerText = selectedMessages.length;\n \n // If no messages selected, automatically exit selection mode\n if (selectedMessages.length === 0) {\n exitSelectionMode();\n }\n}\n\nfunction triggerBulkDelete() {\n if (selectedMessages.length === 0) return;\n \n // Check if ALL selected messages are from admin\n let onlyAdminMessages = selectedMessages.every(m => m.sender === 'account');\n \n if (onlyAdminMessages) {\n // Admin only: Can choose 'Untuk Semua' or 'Hanya Saya'\n Swal.fire({\n title: 'Hapus ' + selectedMessages.length + ' Pesan?',\n text: \"Pilih metode penghapusan.\",\n icon: 'warning',\n showDenyButton: true,\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Semua',\n denyButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-all',\n denyButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeBulkDelete('everyone');\n } else if (result.isDenied) {\n executeBulkDelete('me');\n }\n });\n } else {\n // Mix of Admin + Occupant OR Occupant only: Only 'Hanya Saya'\n Swal.fire({\n title: 'Hapus ' + selectedMessages.length + ' Pesan?',\n text: \"Pesan-pesan ini hanya terhapus pada layar Anda.\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonText: 'Hapus',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeBulkDelete('me');\n }\n });\n }\n}\n\nfunction executeBulkDelete(type) {\n let idsToDelete = selectedMessages.map(m => m.id);\n \n // Dim the UI immediately\n idsToDelete.forEach(id => {\n let el = document.getElementById('msg-' + id);\n if(el) el.style.opacity = '0.5';\n });\n \n $.post('chat_api.php', { \n action: 'bulk_delete_messages', \n id_chats: idsToDelete, \n delete_type: type \n }, function(res) {\n if(res.status === 'success') {\n idsToDelete.forEach(id => {\n $('#msg-' + id).fadeOut(300, function() { $(this).remove(); });\n });\n exitSelectionMode();\n } else {\n // Revert opacity\n idsToDelete.forEach(id => {\n let el = document.getElementById('msg-' + id);\n if(el) el.style.opacity = '1';\n });\n Swal.fire('Gagal', res.message || 'Gagal menghapus beberapa pesan', 'error');\n }\n }, 'json');\n}\n\nfunction editSelectedMessage(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n if (!selectedMsgId || selectedMsgSender !== 'account') return;\n \n editingMsgId = selectedMsgId;\n originalEditMsgText = selectedMsgText;\n \n // Set UI to Edit Mode\n document.getElementById('epText').innerText = originalEditMsgText;\n document.getElementById('editPreview').style.display = 'block';\n \n // If user was replying, cancel it so they don't do both\n closeReplyBox();\n \n let inputEl = document.getElementById('messageInput');\n inputEl.value = originalEditMsgText;\n inputEl.style.height = 'auto';\n inputEl.style.height = (inputEl.scrollHeight) + 'px';\n inputEl.focus();\n}\n\nfunction cancelEdit() {\n editingMsgId = null;\n originalEditMsgText = null;\n document.getElementById('editPreview').style.display = 'none';\n let inputEl = document.getElementById('messageInput');\n inputEl.value = '';\n inputEl.style.height = 'auto';\n}\n\nfunction saveEdit() {\n let input = document.getElementById('messageInput');\n let newText = input.value.trim();\n \n if (!newText || !editingMsgId) return;\n if (newText === originalEditMsgText) {\n cancelEdit();\n return;\n }\n \n let msgElement = document.getElementById('msg-' + editingMsgId);\n if (msgElement) msgElement.style.opacity = '0.5';\n \n let idToEdit = editingMsgId;\n cancelEdit(); // Reset UI immediately\n \n $.post('chat_api.php', { action: 'edit_message', id_chat: idToEdit, new_message: newText }, function(res) {\n if (msgElement) msgElement.style.opacity = '1';\n \n if (res.status === 'success') {\n // Reload messages to ensure formatting consistency\n if (activeOccupantId) {\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n renderMessages(res2.data);\n }\n }, 'json');\n }\n } else {\n Swal.fire('Gagal', res.message || 'Pesan tidak dapat diedit', 'error');\n }\n }, 'json');\n}\n\n// Initial load\n$(document).ready(function() {\n const urlParams = new URLSearchParams(window.location.search);\n const chatId = urlParams.get('id');\n \n if (chatId) {\n openChat(chatId);\n } else {\n loadContacts();\n }\n \n renderEmojis('smileys');\n setInterval(function(){\n if(isSelectionMode) return; // DON'T REFRESH HTML WHILE USER IS SELECTING\n if(!document.getElementById('searchInput').value) {\n loadContacts();\n if(activeOccupantId) {\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n let hist = document.getElementById('chatHistory');\n let newHtml = buildMessagesHtml(res2.data);\n if(hist.innerHTML !== newHtml) {\n let isScrolledBottom = hist.scrollHeight - hist.clientHeight",
"Delete Data",
"Foto wajah Anda telah berhasil tersimpan dengan aman.",
"'+tA(ps.last_seen);try{updCh('cPS',ps);}catch(e){}}\n if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;el('sV').textContent=fm(e.voltage_out)+' V';el('sA').textContent=fm(e.current_out)+' A';el('sW').textContent=fm(e.power_watts)+' W';el('info-sd').innerHTML=mr('mdi-arrow-right','V In',fm(e.voltage_in)+' V','o')+mr('mdi-arrow-left','V Out (5V)',fm(e.voltage_out)+' V','b')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C',tmp>60?'r':'c')+mr('mdi-percent','Efficiency',fm(e.efficiency)+' %','g');el('bar-sd').className='cd-top '+sc(sd.status);el('ft-sd').innerHTML='",
"— beberapa router membatasi perangkat yang boleh terhubung berdasarkan MAC address. Tambahkan MAC address ESP32 ke whitelist jika fitur ini aktif.',\n '",
"Link registrasi sidik jari ini telah kadaluarsa atau tidak valid.",
"query(\"SELECT o.id_occupant, o.name, o.foto, o.status, o.last_active, o.telegram_id,\n (SELECT message FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account ORDER BY c.created_at DESC LIMIT 1) as last_message,\n (SELECT created_at FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account ORDER BY c.created_at DESC LIMIT 1) as last_time,\n (SELECT COUNT(*) FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account AND c.sender = 'occupant' AND c.is_read = 0) as unread_count\n FROM occupant o\n WHERE (SELECT id_chat FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account LIMIT 1) IS NOT NULL \n OR o.telegram_id IS NOT NULL\n ORDER BY last_time DESC, o.name ASC\");\n $contacts = $stmt->fetchAll();\n \n // Add default avatar handling\n foreach ($contacts as &$contact) {\n if (empty($contact['foto']) || !file_exists($contact['foto'])) {\n $contact['foto'] = 'images/faces/user.jpg';\n }\n }\n \n echo json_encode(['status' => 'success', 'data' => $contacts]);\n exit;\n}\n\nif ($action === 'get_messages') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n // Optional parameter to know who is fetching\n $viewer = isset($_POST['viewer']) ? $_POST['viewer'] : 'account'; \n \n if ($viewer === 'occupant') {\n // Update their online status since they are fetching messages\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$id_occupant]);\n }\n \n // Mark messages as read based on who is viewing\n if ($viewer === 'account') {\n $updateStmt = $pdo->prepare(\"UPDATE chats SET is_read = 1 WHERE id_account = ? AND id_occupant = ? AND sender = 'occupant' AND is_read = 0\");\n } else {\n $updateStmt = $pdo->prepare(\"UPDATE chats SET is_read = 1 WHERE id_account = ? AND id_occupant = ? AND sender = 'account' AND is_read = 0\");\n }\n $updateStmt->execute([$id_account, $id_occupant]);\n\n // Fetch messages\n if ($viewer === 'occupant') {\n // If viewed by occupant, don't show deleted ones\n $stmt = $pdo->prepare(\"\n SELECT c.*, r.message as reply_message, r.sender as reply_sender, r.id_chat as reply_id_chat \n FROM chats c \n LEFT JOIN chats r ON c.reply_to_chat_id = r.id_chat \n WHERE c.id_account = ? AND c.id_occupant = ? AND c.deleted_by_occupant = 0 \n ORDER BY c.created_at ASC\n \");\n } else {\n $stmt = $pdo->prepare(\"\n SELECT c.*, r.message as reply_message, r.sender as reply_sender, r.id_chat as reply_id_chat \n FROM chats c \n LEFT JOIN chats r ON c.reply_to_chat_id = r.id_chat \n WHERE c.id_account = ? AND c.id_occupant = ? \n ORDER BY c.created_at ASC\n \");\n }\n $stmt->execute([$id_account, $id_occupant]);\n $messages = $stmt->fetchAll();\n \n // Fetch pinned message\n $pinStmt = $pdo->prepare(\"SELECT * FROM chats WHERE id_account = ? AND id_occupant = ? AND is_pinned = 1 ORDER BY created_at DESC LIMIT 1\");\n $pinStmt->execute([$id_account, $id_occupant]);\n $pinned = $pinStmt->fetch();\n \n // Fetch occupant details for header\n $occStmt = $pdo->prepare(\"SELECT name, foto, status, last_active FROM occupant WHERE id_occupant = ?\");\n $occStmt->execute([$id_occupant]);\n $occupant = $occStmt->fetch();\n if (empty($occupant['foto']) || !file_exists($occupant['foto'])) {\n $occupant['foto'] = 'images/faces/user.jpg';\n }\n \n echo json_encode(['status' => 'success', 'data' => $messages, 'occupant' => $occupant, 'pinned' => $pinned]);\n exit;\n}\n\nif ($action === 'send_message') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n $message = isset($_POST['message']) ? trim($_POST['message']) : '';\n $sender = isset($_POST['sender']) ? $_POST['sender'] : 'account'; \n $reply_to_chat_id = isset($_POST['reply_to_chat_id']) ? (int)$_POST['reply_to_chat_id'] : null;\n \n // Process File Upload if present\n $hasAttachment = false;\n $attachmentPath = \"\";\n $attachmentName = \"\";\n $tgEndpoint = \"sendMessage\";\n $tgFileType = \"\";\n $localHtml = \"\";\n\n if (isset($_FILES['attachment']) && $_FILES['attachment']['error'] === UPLOAD_ERR_OK) {\n $hasAttachment = true;\n $tmpName = $_FILES['attachment']['tmp_name'];\n $attachmentName = $_FILES['attachment']['name'];\n $ext = strtolower(pathinfo($attachmentName, PATHINFO_EXTENSION));\n $newFileName = time() . '_' . rand(1000, 9999) . '.' . $ext;\n \n $uploadDir = __DIR__ . '/images/chat_uploads/';\n if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);\n \n $attachmentPath = $uploadDir . $newFileName;\n move_uploaded_file($tmpName, $attachmentPath);\n \n $urlRelative = \"images/chat_uploads/\" . $newFileName;\n \n // Determine type for local HTML and Telegram API\n $photoExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n $audioExts = ['webm', 'ogg', 'mp3', 'wav', 'oga'];\n if (in_array($ext, $photoExts)) {\n $tgEndpoint = \"sendPhoto\";\n $tgFileType = \"photo\";\n // msg-type-img will be applied via JS if the content only contains this wrap\n $localHtml = \"",
".auth .brand-logo img { width: 140px; max-width: 100%; }\n .auth-form-light {\n background: #ffffff;\n border-radius: 24px;\n padding: 40px 30px;\n text-align: center;\n position: relative;\n z-index: 2;\n margin-top: -30px;\n border: 1px solid rgba(226, 232, 240, 0.8);\n }\n .pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus { border-bottom-color: #4B49AC; }\n .custom-alert {\n position: fixed; top: 20px; right: 20px; background: #fff;\n border-left: 4px solid #fcd34d;\n padding: 15px 20px; border-radius: 4px; z-index: 9999;\n display: none; align-items: center; gap: 12px; min-width: 300px;\n max-width: 400px; animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }\n @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; margin-bottom: 1rem; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }\n @media (max-width: 480px) {\n .custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }\n .pin-box { width: 40px; height: 50px; font-size: 20px; }\n }\n .success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }\n @keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }\n .checkmark-circle { width: 80px; height: 80px; border-radius: 50%; background: #10b981; display: inline-flex; justify-content: center; align-items: center; margin-bottom: 20px; margin-left: auto; margin-right: auto; }\n .checkmark-circle i { color: white; font-size: 40px; line-height: 1; }",
"\"\n data-access=\"",
"Tulis moto atau bio singkat...",
"Format email valid' : '",
"1) {\n oval.classList.remove('ok');\n setStatus('err', 'Terdeteksi banyak wajah');\n consecutiveMatches = 0;\n } else {\n distFill.style.width = '0%';\n oval.classList.remove('ok');\n setStatus('search', 'Posisikan wajah Anda...');\n consecutiveMatches = 0;\n }\n \n }, 300);\n }\n \n function startBrightnessCheck() {\n const tmpCanvas = document.createElement('canvas');\n const tmpCtx = tmpCanvas.getContext('2d');\n \n brightInterval = setInterval(() => {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n \n tmpCanvas.width = 80;\n tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n const data = tmpCtx.getImageData(0, 0, 80, 60).data;\n \n let sum = 0;\n const count = data.length / 4;\n for (let i = 0; i",
"\"error\", \"message\" => \"Method tidak diizinkan\"]);\n exit;\n}\n\nif (!isset($_POST['id_occupant']) || empty($_POST['id_occupant'])) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"ID tidak ditemukan\"]);\n exit;\n}\n\n$id = $_POST['id_occupant'];\n\nif (!is_numeric($id)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"ID tidak valid\"]);\n exit;\n}\n\ntry {\n // Ambil status saat ini\n $get = $pdo->prepare(\"SELECT status FROM occupant WHERE id_occupant = ?\");\n $get->execute([$id]);\n $row = $get->fetch();\n\n if (!$row) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Data tidak ditemukan\"]);\n exit;\n }\n\n // Toggle: approved",
"Memverifikasi...';\n btn.disabled = true;\n \n pinInputs.forEach(inp => inp.disabled = true);\n\n const formData = new FormData(this);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n pinInputs.forEach(inp => inp.classList.add('input-success'));\n // Hide in-card alert if visible\n document.getElementById('inCardAlert').style.display = 'none';\n setTimeout(() => {\n document.getElementById('step1-verify').style.display = 'none';\n document.getElementById('step2-password').style.display = 'block';\n }, 500);\n } else {\n // Show error inside card\n const inCardAlert = document.getElementById('inCardAlert');\n document.getElementById('inCardAlertMsg').textContent = data.message || 'PIN tidak valid!';\n inCardAlert.style.display = 'block';\n inCardAlert.style.animation = 'none';\n setTimeout(() => { inCardAlert.style.animation = 'slideIn 0.3s ease-out'; }, 10);\n \n pinInputs.forEach(inp => {\n inp.classList.add('input-error');\n inp.disabled = false;\n });\n pinInputs[0].focus();\n btn.innerHTML = originalText;\n btn.disabled = false;\n }\n })\n .catch(error => {\n showCustomAlert('Network Error', 'A connection error occurred.', 'error');\n btn.innerHTML = originalText;\n btn.disabled = false;\n pinInputs.forEach(inp => {\n inp.disabled = false;\n if(inp.value !== '') inp.disabled = true; \n });\n });\n });\n\n // Handle Submit Reset Password\n document.getElementById('passwordForm').addEventListener('submit', function(e) {\n e.preventDefault();\n closeCustomAlert();\n\n const pwd = document.getElementById('newPassword').value;\n const confirmPwd = document.getElementById('confirmPassword').value;\n\n if(pwd === '' || confirmPwd === '') {\n showCustomAlert('Incomplete Form', 'Semua kolom wajib diisi.', 'warning');\n return;\n }\n\n const pwValid = (pwd.length >= 8 && /[a-z]/.test(pwd) && /[A-Z]/.test(pwd) && /\\d/.test(pwd) && /[^A-Za-z0-9]/.test(pwd));\n if (!pwValid) {\n showCustomAlert(\"Warning\", \"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!\", \"warning\");\n document.getElementById('newPassword').focus();\n return;\n }\n\n if(pwd !== confirmPwd) {\n showCustomAlert('Failed', 'Passwords do not match.', 'error');\n return;\n }\n\n const btn = document.getElementById('btnUpdate');\n const originalText = btn.innerHTML;\n btn.innerHTML = '",
"\"\n data-status=\"",
"Menyimpan Sidik Jari...';\n \n // Simpan credential ID\n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.style.display = \"none\";\n successWrapper.style.display = \"block\";\n btn.style.display = \"none\";\n } else {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"// Tampilkan error PHP jika ada menggunakan custom alert",
"Pembaruan Profil & Foto Data Diri",
"Password",
"ESP32 mencoba menghubungkan ke jaringan",
"\"\n data-code=\"",
"');\n }\n\n function renderResults(data, query, tab) {\n cachedData = data;\n let html = '';\n const showOverview = tab === 'all' || tab === 'overview';\n const showAccount = tab === 'all' || tab === 'account';\n const showOccupant = tab === 'all' || tab === 'occupant';\n\n // Overview\n if (showOverview && data.overview && data.overview.length > 0) {\n html += '",
"document.querySelectorAll('.eye-toggle').forEach(function(b) { \n b.addEventListener('click', function() { \n var t = document.getElementById(this.dataset.target), i = this.querySelector('i'); \n if (t.type === 'password') { t.type = 'text'; i.classList.replace('mdi-eye-off','mdi-eye'); } \n else { t.type = 'password'; i.classList.replace('mdi-eye','mdi-eye-off'); } \n }); \n});\n\nfunction copyFingerQrLink() {\n navigator.clipboard.writeText(document.getElementById('fingerQrLinkText').value);\n showCustomAlert('Link verifikasi Fingerprint berhasil disalin!');\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Solid white background\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Subtle grid background (like QRIS pattern)\n ctx.strokeStyle = '#f1f5f9';\n ctx.lineWidth = 3;\n var gridSize = 40;\n for (var ix = 0; ix",
"`;});\n const sel=document.getElementById('savedDesigns');sel.innerHTML=opts;\n if(this.designId)sel.value=this.designId;\n sel.onchange=function(){if(this.value!=='0')TFT.loadDesign(this.value);};\n }\n });\n }\n\n loadDesign(id){\n fetch('api_sync.php?action=load&id='+id).then(r=>r.json()).then(res=>{\n if(res.status!=='success')return;\n this.designId=res.data.id;\n document.getElementById('projectName').value=res.data.name;\n const d=JSON.parse(res.data.design_json);\n this.screens=d.screens||[{name:'Screen 1',components:[]}];\n this.orientation=d.orientation||'landscape';\n document.getElementById('orientationSelect').value=this.orientation;\n this.setOrientation(this.orientation);\n this.currentScreen=0;\n this.selectedId=null;\n this.renderCanvas();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.clearProps();\n this.pushHistory();\n });\n }\n\n exportCpp(){\n const data=this.getDesignJSON();\n const fd=new FormData();\n fd.append('action','export_cpp');\n fd.append('design_json',JSON.stringify(data));\n fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{\n if(res.status==='success'){\n const blob=new Blob([res.code],{type:'text/plain'});\n const a=document.createElement('a');a.href=URL.createObjectURL(blob);\n a.download=(data.projectName||'tft_ui')+'.ino';a.click();\n }else alert('❌ '+res.message);\n });\n }\n\n sync(){this.save();alert('✅ Design synced! ESP32 will pick up changes.');}\n\n pullESP(){\n fetch('api_sync.php?action=latest').then(r=>r.json()).then(res=>{\n if(res.status==='success'&&res.elements&&res.elements.length>0){\n // Convert flat elements to component structure\n this.screens[this.currentScreen].components=res.elements.map((el,i)=>({\n id:'c'+(this.idCounter++),type:el.type||'card',x:el.x||0,y:el.y||0,w:el.w||100,h:el.h||50,\n bg:el.fill||'#1e1e30',border:el.strokeWidth||0,borderColor:el.stroke||'#444',\n radius:el.radius||0,padding:4,label:el.type||'Imported',text:el.text||'',\n textColor:el.textColor||'#fff',fontSize:el.fontSize||12,textAlign:'center',\n children:[],interactions:{event:'none',action:'none',target:'',payload:''}\n }));\n this.renderCanvas();this.renderHierarchy();this.pushHistory();\n alert('✅ Pulled from ESP32!');\n }else alert('⚠️ No elements found.');\n });\n }\n}\n\n// === RGB565 Helper ===\nfunction hexToRGB565(hex){\n if(!hex||hex==='transparent')return 0;\n hex=hex.replace('#','');\n if(hex.length===3)hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];\n const r=parseInt(hex.substring(0,2),16);\n const g=parseInt(hex.substring(2,4),16);\n const b=parseInt(hex.substring(4,6),16);\n return((r&0xF8)",
"Please enter the complete 6-digit PIN.",
"Foto berhasil diambil. Klik Save Data untuk menyimpan.",
"`;\n }\n \n menu.style.display = 'block';\n \n let x = e.clientX;\n let y = e.clientY;\n \n // Prevent menu overflowing right edge\n if(x + menu.offsetWidth > window.innerWidth) x = window.innerWidth - menu.offsetWidth - 10;\n \n menu.style.left = x + 'px';\n menu.style.top = y + 'px';\n}\n\nfunction notifAction(action) {\n if(!currCardId) return;\n \n fetch('api_notifications.php', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'action='+action+'&id='+currCardId\n }).then(r => r.json()).then(res => {\n if(res.status==='success') {\n location.reload(); // Quick refresh to update UI accurately matching db\n }\n });\n}\n\ndocument.getElementById('btnEmptyTrash').addEventListener('click', function() {\n if(!confirm('Apakah anda yakin mengosongkan sampah?')) return;\n fetch('api_notifications.php', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'action=empty_trash'\n }).then(()=>location.reload());\n});\n\ndocument.addEventListener('click', function(e) {\n const menu = document.getElementById('ctxMenu');\n if(menu && menu.style.display === 'block') {\n menu.style.display = 'none';\n }\n});",
"Mencari lokasi...",
"function showCustomAlert(message, type = 'success') {\n var alertBox = document.getElementById(\"customCopyAlert\");\n var alertText = document.getElementById(\"customCopyAlertText\");\n var alertIcon = document.getElementById(\"customCopyAlertIcon\");\n \n if (message) {\n alertText.innerHTML = message;\n } else {\n alertText.innerText = \"Berhasil disalin!\";\n }\n \n if (type === 'warning' || type === 'error' || (message && message.toLowerCase().includes('sudah digunakan'))) {\n alertBox.style.backgroundColor = '#f59e0b'; // Amber / Orange elegan\n alertIcon.innerHTML = '",
"\\n\\n\"\n . \"Nomor Anda telah terverifikasi sebagai Penghuni",
"Sidik jari berhasil didaftarkan. Anda dapat menggunakannya untuk login.",
"Semua kolom wajib diisi.",
"\");\n}\n\n$id_occupant = $tokenData['id_occupant'];\n\n// Ambil data occupant\n$stmt = $pdo->prepare(\"SELECT name, password_enc FROM occupant WHERE id_occupant = ?\");\n$stmt->execute([$id_occupant]);\n$occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n\nif (!$occupant) {\n die(\"",
"Harap lengkapi 6 digit PIN Anda.",
"Siap memindai sidik jari Anda...",
":root {\n --primary: #4747a1;\n --primary-light: #e6e6f2;\n --primary-glow: rgba(71, 71, 161, 0.4);\n --success: #4747a1;\n --success-glow: rgba(71, 71, 161, 0.4);\n --warning: #f59e0b;\n --danger: #ef4444;\n --bg-main: #f8fafc;\n --bg-glass: rgba(255, 255, 255, 0.85);\n --text-main: #0f172a;\n --text-muted: #64748b;\n --text-light: #94a3b8;\n --border-soft: rgba(99, 102, 241, 0.15);\n --shadow-glass: 0 8px 32px rgba(30, 41, 59, 0.08);\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg-main);\n font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;\n overflow:hidden; display:flex; align-items:center; justify-content:center;\n -webkit-font-smoothing:antialiased;\n }\n\n /* ── PHONE FRAME ── */\n .phone {\n width:100%; height:100vh;\n background:#1e2223; /* Behind camera */\n display:flex; flex-direction:column;\n position:relative; overflow:hidden;\n }\n\n /* ── HEADER ── */\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:20px 20px;\n z-index:30; position:absolute; top:0; left:0; right:0;\n background: transparent;\n }\n .header-left { display:flex; align-items:center; gap:12px; }\n .header-logo {\n width:32px; height:32px; border-radius:10px;\n background: rgba(255, 255, 255, 0.2); color: #ffffff;\n display:flex; align-items:center; justify-content:center;\n backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);\n }\n .header-logo i { font-size:18px; }\n .header-text { display:flex; flex-direction:column; justify-content:center; gap:2px; }\n .sys-text { font-size:10px; font-weight:600; color:rgba(255,255,255,0.8); text-transform:uppercase; letter-spacing:1px; text-shadow:0 1px 3px rgba(0,0,0,0.5); }\n .sys-sub { font-size:15px; font-weight:700; color:#ffffff; letter-spacing:-0.5px; text-shadow:0 1px 3px rgba(0,0,0,0.5); }\n \n .header-badge {\n font-size:11px; font-weight:600; color:var(--primary);\n background:rgba(255, 255, 255, 0.95); padding:6px 14px; border-radius:20px;\n backdrop-filter: blur(8px);\n max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\n }\n\n /* ── ERROR & LOADING PAGES ── */\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n\n .page-validating, .page-error {\n align-items:center; justify-content:center; gap:20px; color:var(--text-main);\n background: var(--bg-main); z-index: 50; position: absolute; inset: 0;\n }\n .val-ring {\n width:56px; height:56px; border-radius:50%;\n border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .page-validating p { font-size:14px; color:var(--text-muted); font-weight:600; }\n\n .page-error { padding:40px 30px; text-align:center; }\n .err-icon {\n width:80px; height:80px; border-radius:24px;\n display:flex; align-items:center; justify-content:center; margin-bottom:8px;\n box-shadow: 0 10px 30px rgba(0,0,0,0.05);\n }\n .err-icon.expired { background:#fffbeb; border:1px solid #fde68a; color:var(--warning); }\n .err-icon.expired i { font-size:40px; }\n .err-icon.invalid { background:#fef2f2; border:1px solid #fecaca; color:var(--danger); }\n .err-icon.invalid i { font-size:40px; }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text-main); letter-spacing:-0.5px; }\n .page-error p { font-size:14px; color:var(--text-muted); line-height:1.6; max-width:300px; margin-top: 8px; }\n .btn-retry-lib {\n margin-top:24px; padding:12px 32px; border-radius:12px; border:none;\n background:var(--primary); color:#fff; font-family:'Inter', sans-serif;\n font-size:14px; font-weight:700; cursor:pointer; transition:all 0.2s;\n box-shadow: 0 8px 20px var(--primary-glow);\n }\n .btn-retry-lib:hover { transform:translateY(-2px); box-shadow: 0 12px 24px var(--primary-glow); }\n\n /* ── CAMERA AREA ── */\n .cam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n\n /* Elegant Face Oval & Silhouette Mask */\n .face-oval {\n position:absolute; top:0; left:0; width:100%; height:100%;\n pointer-events:none; z-index: 10;\n }\n .face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n #ovalRing { stroke: white; stroke-width: 3; transition:all 0.4s ease; }\n .face-oval.ok #ovalRing { stroke:var(--success); stroke-width: 5; filter:drop-shadow(0 0 12px var(--success-glow)); }\n #ovalRingOut { stroke: rgba(255,255,255,0.4); stroke-width: 8; transition:all 0.4s ease; }\n .face-oval.ok #ovalRingOut { stroke: rgba(75, 73, 172, 0.4); }\n\n /* Cam loading - Clean overlay */\n .cam-loading {\n position:absolute; inset:0; background:rgba(255, 255, 255, 0.95); backdrop-filter:blur(10px);\n display:flex; flex-direction:column; align-items:center; justify-content:center;\n z-index:25; gap:20px; color:var(--text-main); font-family: 'Inter', sans-serif;\n }\n .cam-loading .ldring {\n width:48px; height:48px; border-radius:50%;\n border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n .cam-loading p { font-size:14px; color:var(--text-muted); font-weight:600; text-align:center; }\n .cam-loading .load-progress {\n width:200px; height:6px; background:var(--border-soft); border-radius:4px; overflow:hidden;\n }\n .cam-loading .load-progress-fill {\n height:100%; width:0%; background:var(--primary); transition:width 0.4s ease; border-radius:4px;\n }\n\n /* Dashboard Container */\n .hud-dashboard {\n position:absolute; bottom:0; left:0; right:0; z-index:20;\n display:flex; flex-direction:column; align-items:center; padding-bottom:30px;\n pointer-events:none; \n }\n \n /* Glassmorphism Instructions Panel */\n .instructions {\n background:rgba(255, 255, 255, 0.9); border:1px solid rgba(255,255,255,0.5);\n padding:8px 10px; border-radius:12px; box-shadow:0 4px 15px rgba(0,0,0,0.05);\n width: 85%; max-width: 250px; margin-bottom: 24px; position:relative; pointer-events:auto;\n backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);\n }\n .instr-list { list-style:none; display:grid; grid-template-columns: 1fr 1fr; gap:6px 8px; }\n .instr-list li { font-size:8.5px; color:var(--text-main); font-weight:600; display:flex; align-items:center; gap:6px; }\n .instr-list li i { font-size:11px; color:var(--primary); flex-shrink:0; background:var(--primary-light); padding:3px; border-radius:4px; }\n\n /* Sleek Status pill */\n .st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; z-index:12; white-space:nowrap;\n background:var(--bg-glass); border:1px solid rgba(255,255,255,0.5); color:var(--text-main);\n backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { color:var(--text-muted); }\n .st-pill.searching i { color:var(--primary); }\n .st-pill.found { background:var(--success); color:#fff; border-color:var(--success); }\n .st-pill.found i { color:#fff; }\n .st-pill.warn { background:var(--danger); color:#fff; border-color:var(--danger); }\n .st-pill.warn i { color:#fff; }\n\n /* ── CAPTURE & ACTION BARS ── */\n .bar-capture {\n position:relative; width:100%; z-index:20;\n display:flex; align-items:flex-end; justify-content:center;\n margin-top:-10px;\n }\n .shutter {\n width:76px; height:76px; border-radius:50%; pointer-events:auto;\n border:4px solid rgba(255,255,255,0.4); background:rgba(255,255,255,0.1); cursor:pointer;\n display:flex; align-items:center; justify-content:center;\n transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position:relative;\n backdrop-filter:blur(4px);\n }\n .shutter::after {\n content:''; width:56px; height:56px; border-radius:50%;\n background:#ffffff; transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n }\n .shutter:hover::after { transform:scale(0.95); }\n .shutter:active::after { transform:scale(0.8); background:var(--primary); }\n .shutter:disabled { opacity:0.6; cursor:not-allowed; pointer-events:none; }\n .shutter-lbl { display:none; }\n\n .bar-after {\n position:absolute; bottom:0; left:0; right:0; z-index:40;\n padding:30px 0; background: rgba(255,255,255,0.95); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);\n display:flex; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226, 232, 240, 0.8);\n border-top-left-radius:24px; border-top-right-radius:24px;\n pointer-events:auto; display:none;\n }\n .act-btn { \n display:flex; flex-direction:column; align-items:center; gap:8px; \n border:none; background:none; cursor:pointer; transition:0.2s;\n pointer-events:auto; outline:none; -webkit-tap-highlight-color: transparent;\n }\n .act-btn .ring { \n width:56px; height:56px; border-radius:18px;\n display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.2s ease;\n }\n .act-btn span { font-size:11px; font-weight:700; color:var(--text-main); text-transform:uppercase; letter-spacing:0.5px; }\n \n .act-btn.retry .ring { background:#f1f5f9; color:var(--text-muted); border:1px solid #e2e8f0; }\n .act-btn.retry:hover .ring { background:#e2e8f0; }\n \n .act-btn.save .ring { background:var(--success); color:#fff; }\n .act-btn.save:hover .ring { opacity:0.9; }\n\n /* ── PREVIEW ── */\n .preview { flex:1; position:relative; background:#1e2223; overflow:hidden; }\n .preview canvas { width:100%; height:100%; object-fit:cover; }\n .preview-tag {\n position:absolute; top:85px; left:50%; transform:translateX(-50%);\n background:var(--success); color:#fff; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px;\n z-index:5;\n }\n .preview-tag i { font-size:18px; }\n .preview-watermark {\n position:absolute; bottom:140px; right:16px; font-size:10px; color:rgba(255,255,255,0.8);\n font-weight:600; z-index:5; text-align:right; line-height:1.5; text-shadow:0 1px 4px rgba(0,0,0,0.8);\n }\n\n .toast {\n position:absolute; top:20px; left:50%; transform:translateX(-50%);\n padding:12px 24px; font-size:13px; font-weight:700; border-radius:16px;\n display:none; align-items:center; gap:10px; z-index:60; white-space:nowrap; animation:slideDown 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n background:var(--bg-glass); backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.6);\n }\n .toast i { font-size:20px; }\n .toast.ok { color:var(--success); }\n .toast.err { color:var(--danger); }\n @keyframes slideDown { from{opacity:0;transform:translateX(-50%) translateY(-20px);} to{opacity:1;transform:translateX(-50%) translateY(0);} }\n\n /* ── SUCCESS PAGE - ENHANCED ── */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text-main); background: var(--bg-main); z-index:90; position:absolute; inset:0; }\n .suc-card {\n background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 32px; padding: 40px 30px;\n box-shadow: none; display: flex; flex-direction: column; align-items: center;\n max-width: 90%; animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n position: relative; overflow: hidden;\n }\n @keyframes slideUpFade { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }\n \n .suc-ring-wrap {\n width: 90px; height: 90px; border-radius: 50%; background: var(--primary-light);\n display: flex; align-items: center; justify-content: center; margin-bottom: 20px;\n position: relative;\n }\n .suc-ring-wrap::before {\n content: ''; position: absolute; inset: -10px; border-radius: 50%;\n border: 2px solid var(--primary-glow); animation: pulseSurround 2s infinite;\n }\n .suc-ring {\n width: 60px; height: 60px; border-radius: 50%; background: var(--success);\n display:flex; align-items:center; justify-content:center;\n z-index: 2;\n }\n .suc-ring i { font-size: 32px; color: #fff; }\n @keyframes pulseSurround { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }\n \n .page-success h2 { font-size: 24px; font-weight: 800; color: var(--success); letter-spacing: -0.5px; margin-bottom: 12px; }\n .page-success p { font-size: 14px; color: var(--text-muted); line-height: 1.6; max-width: 280px; margin-bottom: 20px; }\n .suc-foto { \n width:120px; height:120px; border-radius:24px; object-fit:cover; border:4px solid #fff; \n margin-top:10px; margin-bottom: 24px;\n background: var(--primary-light);\n }\n .suc-checks-wrap { display: flex; flex-direction: column; gap: 12px; background: rgba(241, 245, 249, 0.6); padding: 16px 20px; border-radius: 16px; width: 100%; }\n .suc-check { display:flex; align-items:flex-start; gap:12px; font-size:13px; color:var(--text-main); font-weight:600; text-align: left;}\n .suc-check i { color:var(--success); font-size:18px; margin-top: -2px; }\n .suc-countdown { font-size:12px; color:var(--text-muted); margin-top:24px; font-weight:600; background:rgba(255,255,255,0.8); padding: 6px 16px; border-radius: 20px; }\n\n /* ── APP-LIKE METERS ── */\n .brightness-meter, .liveness-meter {\n position:absolute; top:50%; transform:translateY(-50%);\n display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12;\n padding: 14px 10px; background: none; border-radius: 0; backdrop-filter: none;\n }\n .brightness-meter { right:6px; }\n .liveness-meter { left:6px; }\n \n .bm-icon, .lm-icon { font-size:16px; color:#fff; }\n \n .bm-bar, .lm-bar { \n width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden;\n border-radius:6px;\n }\n \n .bm-fill, .lm-fill { \n position:absolute; bottom:0; left:0; width:100%; border-radius:6px;\n background: #fff; transition:height 0.4s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.4s ease;\n }\n .bm-label, .lm-label { display:none; }",
"showCustomAlert(\"Setup PIN Gagal\", \"",
"Current Password",
"Pendaftaran Dibatalkan / Gagal (' + err.message + ')';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n }\n}",
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalEdit\").modal(\"hide\"); \n document.getElementById(\"editFotoOccupantId\").value = document.getElementById(\"edit_id\").value;\n document.getElementById(\"editFotoPreview\").src = currentEditFoto || 'images/faces/user.jpg';\n document.getElementById(\"editFotoDefaultView\").style.display = \"block\";\n document.getElementById(\"editFotoLoadingView\").style.display = \"none\";\n document.getElementById(\"editFotoQrView\").style.display = \"none\";\n $(\"#modalEditFoto\").modal(\"show\");\n }\n else { showCustomAlert(d.message || \"Gagal menyimpan perubahan.\"); }\n }).catch(function(){ btn.disabled=false; btn.innerHTML='",
"Sampah (30 Hari)",
"Kosongkan jika tidak ubah",
"Detection Error';\n });\n }\n if (!faceModelLoadedEdit) {\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedEdit = true;\n doDetect();\n });\n } else {\n doDetect();\n }\n };\n };\n reader.readAsDataURL(file);\n });\n \n // Reset face detection when edit modal closes\n $('#modalEdit').on('hidden.bs.modal', function() {\n document.getElementById('faceVerifiedFlagEdit').value = '1';\n document.getElementById('faceDetectPreviewEdit').style.display = 'none';\n document.getElementById('faceDetectBadgeEdit').style.display = 'none';\n });\n\n});",
"0.6;\n\n // Face center validation\n var cx = box.x + box.width / 2;\n var cy = box.y + box.height / 2;\n var dx = Math.abs(cx - vw / 2) / vw;\n var dy = Math.abs(cy - vh / 2) / vh;\n var faceNotCenter = dx > 0.2 || dy > 0.2;\n\n if (faceTooSmall) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu jauh — Dekatkan');\n } else if (faceTooBig) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu dekat — Mundur');\n } else if (faceNotCenter) {\n setFaceNotOk();\n setStatus('err', 'Pusatkan wajah dalam bingkai');\n } else {\n // Face is good\n faceOk = true;\n oval.classList.add('ok');\n corners.classList.add('ok');\n scanbar.classList.add('ok');\n\n // Liveness: track landmark movement\n if (landmarks && landmarkLoaded) {\n var pts = landmarks.positions;\n if (prevLandmarks && pts.length === prevLandmarks.length) {\n var totalDiff = 0;\n for (var i = 0; i",
"Sisa waktu: \" . $timeStr . \"",
"Confirm New Password",
"document.querySelectorAll('.eye-toggle').forEach(function(b) { b.addEventListener('click', function() { var t = document.getElementById(this.dataset.target), i = this.querySelector('i'); if (t.type === 'password') { t.type = 'text'; i.classList.replace('mdi-eye-off','mdi-eye'); } else { t.type = 'password'; i.classList.replace('mdi-eye','mdi-eye-off'); } }); });\n\nvar currentQrUrl = '';\n\n// Generate QR after tambah (in Code Register modal)\nfunction generateQrInCodeModal(idOccupant) {\n var area = document.getElementById('qrCodeArea');\n var canvas = document.getElementById('qrCodeCanvas');\n area.style.display = 'none';\n canvas.innerHTML = '';\n\n var fd = new FormData();\n fd.append('id_occupant', idOccupant);\n fetch('generate_foto_token.php', { method: 'POST', body: fd })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n if (d.status === 'success') {\n area.style.display = 'block';\n currentQrUrl = d.url;\n document.getElementById('qrLinkText').value = d.url;\n new QRCode(canvas, {\n text: d.url,\n width: 200,\n height: 200,\n colorDark: '#0f172a',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n }\n }).catch(function(){});\n}\n\nfunction copyQrCodeRegisterLink() {\n navigator.clipboard.writeText(document.getElementById('qrLinkText').value);\n showCustomAlert('Link berhasil disalin!');\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Solid white background\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Subtle grid background (like QRIS pattern)\n ctx.strokeStyle = '#f1f5f9';\n ctx.lineWidth = 3;\n var gridSize = 40;\n for (var ix = 0; ix",
"');\n }\n }, 'json');\n}\n\nfunction openChat(id, name, foto, status) {\n currentOccupantId = id;\n $('#activeOccupantId').val(id);\n $('#activeChatName').text(name);\n $('#activeChatAvatar').attr('src', foto);\n $('#activeChatStatus').text(status === 'approved' ? 'Terverifikasi' : status);\n \n // Highlight contact\n $('.contact-item').removeClass('active');\n event.currentTarget.classList.add('active');\n \n // Toggle UI\n $('#chatEmptyState').hide();\n $('#chatHeader').css('display', 'flex').attr('style', 'border-radius: 10px 10px 0 0; display: flex !important;');\n $('#chatHistory').css('display', 'flex');\n $('#chatFooter').show();\n \n fetchMessages();\n}\n\nfunction fetchMessages() {\n if (currentOccupantId === 0) return;\n $.post('chat_api.php', { action: 'get_messages', id_occupant: currentOccupantId, viewer: 'account' }, function(res) {\n if (res.status === 'success') {\n let container = document.getElementById('chatHistory');\n let isScrolledBottom = container.scrollHeight - container.clientHeight",
"\"\n data-name=\"",
"';}\n\nfunction buildAll(){try{bCtrl();}catch(e){console.warn(e);}try{bAcc();}catch(e){console.warn(e);}try{bInd();}catch(e){console.warn(e);}try{bPow();}catch(e){console.warn(e);}}\nfunction updateAll(){try{uCtrl();}catch(e){}try{uAcc();}catch(e){}try{uInd();}catch(e){}try{uPow();}catch(e){}}\n\n// CONTROLLER\nfunction bCtrl(){\n const c=F('esp32');if(!c)return;const e=c.extra_data||{};\n el('gCtrl').innerHTML=`",
"Posisikan wajah di oval",
"Memuat AI Model & Kamera...",
"`;\n}\nfunction uCtrl(){\n const c=F('esp32');if(!c)return;const e=c.extra_data||{};\n const b=el('bar-esp');if(b)b.className='cd-top '+sc(c.status);\n const bd=el('bdg-esp');if(bd){bd.className='bd '+sc(c.status);bd.textContent=sl(c.status);}\n const cn=el('esp-conn');if(cn)cn.innerHTML=mr('mdi-wifi','WiFi',e.wifi_status==='on'?'CONNECTED':'DISCONNECTED',e.wifi_status==='on'?'g':'r')+mr('mdi-signal','Signal',wfb(e.wifi_rssi)+' '+E(c,'wifi_rssi')+' dBm','')+mr('mdi-access-point','SSID',e.wifi_ssid||'—','b')+mr('mdi-ip-network','IP',e.wifi_ip||'—','c');\n const sy=el('esp-sys');if(sy)sy.innerHTML=mr('mdi-speedometer','CPU',E(c,'cpu_freq')+' MHz','b')+mr('mdi-memory','Heap',fH(e.free_heap),'c')+mr('mdi-clock-fast','Uptime',fUp(e.uptime),'g')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','o')+mr('mdi-flash','V Input',fm(e.voltage_in)+' V','o');\n const f=el('ft-esp');if(f)f.innerHTML='",
"Pilih minimal satu data untuk dihapus!",
"\"\n data-icnclass=\"",
"— ESP32 hanya mendukung frekuensi 2.4GHz. Jika router hanya memancarkan 5GHz, ESP32 tidak akan bisa mendeteksinya.'\n ];\n } else if (reason.indexOf('weak_signal') !== -1) {\n errMsg = 'Sinyal WiFi Terlalu Lemah (Weak Signal)';\n reasonText = '",
"WebAuthn tidak didukung di browser ini.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\", id: window.location.hostname },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { \n authenticatorAttachment: \"platform\", \n userVerification: \"required\",\n residentKey: \"required\",\n requireResidentKey: true\n },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
"Status pintu IoT dan waktu akses terakhir.",
"tidak",
"Change Password",
"Pendaftaran Gagal",
"';\n }\n\n var sN = String(d.name).replace(/'/g, \"\\\\'\");\n var sNik = String(d.nik || '').replace(/'/g, \"\\\\'\");\n var sHp = String(d.no_hp).replace(/'/g, \"\\\\'\");\n var sFoto = String(d.foto || '').replace(/'/g, \"\\\\'\");\n\n var html = '' +\n'",
"prepare(\"UPDATE account SET password = ?, password_enc = ? WHERE username = ?\");\n if ($stmt->execute([$password_hash, $password_enc, $username])) {\n unset($_SESSION['reset_username']);\n $success = \"Password berhasil diubah. Mengalihkan ke halaman login...\";\n } else {\n $error = \"Failed mengubah password.\";\n $step = 2;\n }\n } catch (PDOException $e) {\n $error = \"Database Error.\";\n $step = 2;\n }\n }\n } elseif (isset($_POST['action']) && $_POST['action'] === 'verify_pin') {\n $username = $_POST['username'] ?? '';\n $pin = $_POST['pin'] ?? '';\n\n if (empty($username) || empty($pin) || strlen($pin)",
"1) opacity = 1;\n \n let thickness = 1 + (zFactor * 3.5);\n\n // Warna yang sinkron berombak menjalar\n const colorPhase = time * 0.1 + (x3 * 0.3 + y3 * 0.2); \n const c = getColorAtTime(colorPhase);\n\n if (!this.isFirstFrame && opacity > 0.05) {\n ctx.save();\n ctx.strokeStyle = `rgba(${c.r}, ${c.g}, ${c.b}, ${opacity})`;\n ctx.lineWidth = thickness;\n ctx.lineCap = 'round';\n \n ctx.beginPath();\n ctx.moveTo(this.prevScreenX, this.prevScreenY);\n \n // Jarak gerak layar (seberapa jauh titik berpindah dari frame sebelumnya)\n const moveDist = Math.hypot(screenX - this.prevScreenX, screenY - this.prevScreenY);\n \n // Jika perpindahannya terlalu kecil (karena sedang di pinggir batas rotasi), \n // kita paksakan dia punya buntut kecil agar tetap terlihat seperti 'dash/garis' \n // bukan 'titik biasa', persis seperti di Antigravity.\n if (moveDist",
"Real-time door status",
"Pesan peringatan di sini.",
"Tidak ada notifikasi.",
"/* Custom Alert dari atas */\n#customCopyAlert {\n position: fixed;\n top: -100px;\n left: 50%;\n transform: translateX(-50%);\n background-color: #06c283ff; /* Warna hijau success */\n color: white;\n padding: 12px 24px;\n border-radius: 8px;\n font-size: 14px;\n font-weight: bold;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n transition: top 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n z-index: 10000;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n#customCopyAlert.show {\n top: 24px;\n}\n#customCopyAlert svg {\n width: 20px;\n height: 20px;\n}",
"dengan password yang dikonfigurasi pada access point/router tersebut.' ;\n suggestions = [\n '",
"Data Access Occupant",
"Edit Social Media",
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_account', id);\n return fetch('delete_account.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_account.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteAccount\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n\n if ($.fn.DataTable.isDataTable('#myTable')) { \n $('#myTable').DataTable().destroy(); \n }\n var table = $('#myTable').DataTable({ \n \"columnDefs\": [{ \"orderable\": false, \"targets\": 4 }, { \"width\": \"40px\", \"targets\": 4 }],\n \"order\": [[0, 'asc']]\n });\n\n $('#edit_password').on('input', function() {\n var pass = $(this).val();\n var meter = $('#pwStrengthMeterEdit');\n var bar = $('#pwStrengthBarEdit');\n var text = $('#pwStrengthTextEdit');\n\n if(pass.length > 0) {\n meter.show();\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.css({width: '100%', backgroundColor: '#10b981'}); // Kuat\n text.text('Password Kuat').css('color', '#10b981');\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.css({width: '66%', backgroundColor: '#f59e0b'}); // Sedang\n text.text('Password Sedang').css('color', '#f59e0b');\n } else {\n bar.css({width: '33%', backgroundColor: '#ef4444'}); // Lemah\n text.text('Password Lemah').css('color', '#ef4444');\n }\n } else {\n meter.hide();\n text.text('');\n }\n });\n\n // Eye Toggle Logic\n $('.eye-toggle').on('click', function() {\n var targetId = $(this).data('target');\n var input = $('#' + targetId);\n var icon = $(this).find('i');\n \n if (input.attr('type') === 'password') {\n input.attr('type', 'text');\n icon.removeClass('mdi-eye-off').addClass('mdi-eye');\n } else {\n input.attr('type', 'password');\n icon.removeClass('mdi-eye').addClass('mdi-eye-off');\n }\n });\n\n // ── Face Detection on Photo Upload (Edit Modal) ──\n var faceModelLoadedEdit = false;\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedEdit = true;\n });\n \n $('#edit_foto').on('change', function() {\n var file = this.files[0];\n if (!file) {\n document.getElementById('faceVerifiedFlagEdit').value = '1';\n document.getElementById('faceDetectPreviewEdit').style.display = 'none';\n return;\n }\n \n var preview = document.getElementById('faceDetectPreviewEdit');\n var badge = document.getElementById('faceDetectBadgeEdit');\n \n preview.style.display = 'block';\n badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#e2e8f0; color:#475569; border:1px solid #cbd5e1;';\n badge.innerHTML = '",
"Kembali",
"1. Foto Kegiatan (Auto: Waktu & Lokasi)",
"Kata sandi Anda telah diperbarui.",
"/forgot_pin",
"window.Telegram.WebApp.ready();\n window.Telegram.WebApp.expand();\n\n function previewImage(input) {\n if (input.files && input.files[0]) {\n var reader = new FileReader();\n reader.onload = function(e) {\n document.getElementById('previewFoto').src = e.target.result;\n }\n reader.readAsDataURL(input.files[0]);\n }\n }\n\n function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n setTimeout(() => closeCustomAlert(), 5000);\n }\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => { alertBox.style.display = 'none'; }, 300);\n }\n\n // Navigation Handlers\n const infoAlertText = document.querySelector('.info-alert-text');\n const originalInfoText = infoAlertText.innerHTML;\n \n document.getElementById('btnLanjut').addEventListener('click', function() {\n const name = document.getElementById('name').value.trim();\n if(name === '') {\n showCustomAlert('Incomplete Form', 'Full Name wajib diisi.', 'warning');\n return;\n }\n document.getElementById('step1-data').style.display = 'none';\n document.getElementById('step2-foto').style.display = 'block';\n infoAlertText.innerHTML = 'Silakan klik foto di bawah ini untuk memperbarui foto. Jika",
"Token QR code ini sudah pernah digunakan dan tidak bisa dipakai lagi.",
"Halaman akan ditutup otomatis...",
"'+tA(fp.last_seen);}\n if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';el('info-so').innerHTML=`",
"Status Pintu",
"');\n \n $.post('chat_api.php', { action: 'send_message', id_occupant: currentOccupantId, message: text, sender: 'account' }, function(res) {\n if (res.status === 'success') {\n $('#chatInput').val('');\n fetchMessages();\n fetchContacts();\n } else {\n showCustomAlert(res.message);\n }\n $('#chatInput').prop('disabled', false).focus();\n submitBtn.prop('disabled', false).html('",
"*/\n .sidebar .nav .sub-menu .nav-item .nav-link::before {\n content: \"\" !important;\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background-color: #a3a4a5;\n margin-right: 10px;\n flex-shrink: 0;\n }\n \n /* Menggeser (menarik) letak kumpulan submenu agak lebih ke kiri jika dibutuhkan */\n .sidebar .nav .sub-menu {\n padding-left: 1.5rem !important;\n padding-top: 2px !important;\n padding-bottom: 2px !important;\n margin-top: -1px !important;\n margin-bottom: -1px !important;\n }\n .sidebar .nav .sub-menu .nav-item .nav-link {\n padding-top: 10px !important;\n padding-bottom: 10px !important;\n }\n .sidebar .nav .nav-item .collapse,\n .sidebar .nav .nav-item .collapsing {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n \n /* 7. SAAT SUBMENU AKTIF ATAU DIKLIK (warna teks + titik membulat menjadi biru pudar) */\n .sidebar .nav .sub-menu .nav-item .nav-link.active,\n .sidebar .nav .sub-menu .nav-item .nav-link:hover,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link.active,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link:hover {\n color: #4B49AC !important;\n font-weight: 500 !important;\n }\n \n /* Titik bulat berubah biru jika sub-menu tersebut aktif */\n .sidebar .nav .sub-menu .nav-item .nav-link.active::before,\n .sidebar .nav .sub-menu .nav-item .nav-link:hover::before,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link.active::before,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link:hover::before {\n background-color: #4B49AC !important;\n }\n\n /* ========================================================= */\n /* CHAT SIDEBAR MENU STYLING */\n /* ========================================================= */\n .contact-item { display: flex; padding: 12px 20px; gap: 12px; cursor: pointer; transition: 0.2s; align-items: center; text-decoration: none; border-bottom: 1px solid #f0f0f0; }\n .contact-item:hover { background: #f8faff; text-decoration: none; }\n .contact-item .ci-avatar { width: 42px; height: 42px; border-radius: 50%; position: relative; flex-shrink: 0; }\n .contact-item .ci-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }\n .contact-item .ci-status { position: absolute; bottom: 2px; right: 2px; width: 10px; height: 10px; border-radius: 50%; background: #10b981; border: 2px solid #fff; }\n .contact-item .ci-info { flex: 1; min-width: 0; display:flex; flex-direction:column; justify-content:center; }\n .contact-item .ci-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }\n .contact-item .ci-name { font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .contact-item .ci-time { font-size: 11px; color: #999; flex-shrink: 0; }\n .contact-item .ci-msg { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }\n .contact-item .ci-unread-badge { background: #5856d6; color: #fff; font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 10px; margin-left: 6px; display: inline-block; vertical-align: middle; }\n\n /* ========================================================= */\n /* HEADER SEARCH DROPDOWN */\n /* ========================================================= */\n .search-dropdown {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n width: 420px;\n background: #ffffff;\n border: 1px solid #e8e8e8;\n border-radius: 12px;\n box-shadow: 0 12px 40px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.06);\n z-index: 9999;\n overflow: hidden;\n animation: searchDropIn 0.2s ease;\n }\n @keyframes searchDropIn {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n }\n .search-tabs {\n display: flex;\n gap: 4px;\n padding: 12px 16px 0;\n border-bottom: 1px solid #f0f0f0;\n }\n .search-tab-item {\n background: none;\n border: none;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 500;\n color: #888;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n transition: all 0.2s ease;\n border-radius: 6px 6px 0 0;\n outline: none;\n }\n .search-tab-item:hover {\n color: #4B49AC;\n background: #f6f5ff;\n }\n .search-tab-item.active {\n color: #4B49AC;\n border-bottom-color: #4B49AC;\n font-weight: 600;\n }\n .search-results {\n max-height: 360px;\n overflow-y: auto;\n padding: 8px 0;\n }\n .search-results::-webkit-scrollbar { width: 4px; }\n .search-results::-webkit-scrollbar-track { background: transparent; }\n .search-results::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n .search-section-title {\n padding: 8px 16px 4px;\n font-size: 10px;\n font-weight: 700;\n color: #aaa;\n letter-spacing: 1px;\n text-transform: uppercase;\n }\n .search-result-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 16px;\n cursor: pointer;\n transition: background 0.15s ease;\n text-decoration: none;\n color: inherit;\n }\n .search-result-item:hover {\n background: #f8f7ff;\n text-decoration: none;\n color: inherit;\n }\n .search-result-icon {\n width: 38px;\n height: 38px;\n border-radius: 10px;\n background: #f1f5f9;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n }\n .search-result-icon i {\n font-size: 16px;\n color: #101112ff;\n }\n .search-result-avatar {\n width: 38px;\n height: 38px;\n border-radius: 50%;\n object-fit: cover;\n flex-shrink: 0;\n border: 2px solid #f0f0f0;\n }\n .search-result-info {\n flex: 1;\n min-width: 0;\n }\n .search-result-name {\n font-size: 13.5px;\n font-weight: 600;\n color: #2c2e33;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n }\n .search-result-sub {\n font-size: 11.5px;\n color: #999;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n }\n .search-result-arrow {\n color: #ccc;\n font-size: 14px;\n flex-shrink: 0;\n transition: color 0.15s;\n }\n .search-result-item:hover .search-result-arrow {\n color: #4B49AC;\n }\n .search-empty {\n text-align: center;\n padding: 30px 16px;\n color: #bbb;\n font-size: 13px;\n }\n .search-highlight {\n background: #e8e5ff;\n border-radius: 2px;\n padding: 0 1px;\n }",
"Ambil Foto",
"WebAuthn tidak didukung di browser ini.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\" },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { authenticatorAttachment: \"platform\", userVerification: \"required\" },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
"Peringatan",
"'+tA(rl.last_seen);}\n}\n\n// INDICATORS\nfunction bInd(){\n const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');let h='';\n if(tf){const e=tf.extra_data||{};const act=(e.display_state||'active')==='active';\n h+=`",
"— beberapa router memiliki fitur AP Isolation atau Client Isolation yang mencegah perangkat baru terhubung. Nonaktifkan fitur ini di pengaturan router.'\n ];\n } else {\n errMsg = 'Koneksi Gagal (Alasan Tidak Diketahui)';\n reasonText = '",
"ID: ' + TOKEN.substring(0, 8) + '...';\n }\n\n dataURL = capCanvas.toDataURL('image/png');\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n camArea.style.display = 'none';\n previewWrap.classList.remove('page-hidden');\n previewWrap.style.display = 'block';\n barCapture.style.display = 'none';\n barAfter.style.display = 'flex';\n\n var st = document.getElementById('simpleText');\n if (st) st.style.display = 'none';\n var hr = document.querySelector('.header-right');\n if (hr) hr.style.display = 'none';\n }\n\n // ── Retake ──\n function retake() {\n dataURL = null;\n livenessScore = 0; livenessFrames = 0; prevLandmarks = null;\n lmFill.style.height = '0%';\n previewWrap.classList.add('page-hidden');\n previewWrap.style.display = '';\n camArea.style.display = 'block';\n barAfter.style.display = 'none';\n barCapture.style.display = 'flex';\n \n var st = document.getElementById('simpleText');\n if (st) st.style.display = 'block';\n var hr = document.querySelector('.header-right');\n if (hr) hr.style.display = 'block';\n toast.style.display = 'none';\n camLoading.style.display = 'flex';\n loadText.textContent = 'Memuat ulang kamera...';\n setProgress(50);\n startCamera().then(function(ok) {\n if (ok) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }\n });\n }\n\n // ── Save ──\n function save() {\n if (!dataURL || sessionSaved) return;\n btnSave.querySelector('.ring').innerHTML = '",
"Simpan'; \n// showCustomAlert(\"Koneksi ke server gagal.\"); \n// });\n// });\n\nfunction openDeleteModal(id, name) { \n document.getElementById(\"delete_id_account\").value = id; \n document.getElementById(\"delete_account_name\").innerText = name; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n}\n\nfunction toggleStatus(id) { \n $.post(\"toggle_status_account.php\", { id_account: id }, function(d) { \n if (d.status === \"success\") location.reload(); \n else showCustomAlert(d.message || \"Gagal memproses.\"); \n }, \"json\"); \n}\n\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
"to edit properties",
"Edit Account",
"Download Foto",
"');\n selectElement.prop('disabled', true);\n\n $.ajax({\n url: 'update_access_occupant.php',\n type: 'POST',\n dataType: 'json',\n data: {\n id_occupant: idOccupant,\n access: accessValue\n },\n success: function(response) {\n selectElement.prop('disabled', false);\n btn.html('",
"Menyimpan...';\n \n fetch(\"tambah_account.php\", { method: \"POST\", body: new FormData(this) })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
"Waktu akses (5 menit) telah habis. Silakan gunakan perintah /edit_data di Telegram lagi.",
"untuk terhubung secara personal dengan Admin Identia.\\n\\n\"\n . \"Namun, untuk memulai obrolan langsung dengan Admin, kami perlu memverifikasi data Anda terlebih dahulu.\\n\\n\"\n . \"👇",
"Yakin menghapus item yang dipilih secara permanen?",
"Sudah Terdaftar",
"r.json())\n .then(d => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n if(d.status === 'success') {\n document.getElementById(\"fingerprintQrView\").style.display = 'block';\n document.getElementById(\"fingerQrLinkText\").value = d.url;\n var canvas = document.getElementById(\"fingerprintQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url, width: 150, height: 150,\n colorDark: '#0f172a', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n }\n }).catch(e => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n });\n}\n\nasync function startWebAuthnRegistration() {\n var btn = document.getElementById(\"btnScanFingerHere\");\n var statusEl = document.getElementById(\"webAuthnStatus\");\n var errorEl = document.getElementById(\"webAuthnError\");\n var successWrapper = document.getElementById(\"webAuthnStatusWrapper\");\n var accountId = document.getElementById(\"fingerprintAccountId\").value;\n \n btn.disabled = true;\n errorEl.style.display = \"none\";\n successWrapper.style.display = \"none\";\n \n statusEl.innerHTML = '",
"1) {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n btnShutter.disabled = true;\n } else {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n }\n });\n }, 280);\n }\n\n /* ── Brightness Check ────────── */\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n fd.append('liveness_score', Math.round(livenessScore));\n fd.append('capture_time', new Date().toISOString());\n fd.append('device_info', navigator.userAgent);\n fd.append('screen_res', screen.width + 'x' + screen.height);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n sessionSaved = true;\n clearInterval(tokenCheckInterval);\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n // Auto-close countdown\n var count = 5;\n var cdEl = document.getElementById('sucCountdown');\n if (cdEl) cdEl.innerText = \"Sesi ini akan otomatis ditutup dalam \" + count + \" detik...\";\n var cdInterval = setInterval(function() {\n count--;\n if (cdEl) cdEl.innerText = \"Sesi ini akan otomatis ditutup dalam \" + count + \" detik...\";\n if (count",
"'; });\n $('#saveFailSuggestions').html(sugHtml);\n \n if (fallbackHtml) {\n $('#saveFailFallbackInfo').html(fallbackHtml).show();\n } else {\n $('#saveFailFallbackInfo').hide();\n }\n \n // Widen modal for detailed failure view\n $('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');\n $('#saveStep5').fadeIn(300);\n \n } else if (statusRes.status === 'success') {\n clearInterval(changeCheckInterval);\n \n $('#saveStep2 .save-bar-fill').css({'width': '100%', 'transition': 'width 0.5s'}); // Cepat penuhi\n \n setTimeout(function() {\n // Lanjutkan ke Success View\n $('.save-step').hide();\n $('#saveStep2 .save-bar-fill').css({'width': '0%', 'transition': 'none'}); // Reset for next\n $('#saveNewSsidDisplay').html('",
"Belum ada riwayat percakapan. Mulai kirim chat!",
"— dinding beton, lemari logam, dan cermin dapat menyerap atau memantulkan sinyal WiFi 2.4GHz secara signifikan. Posisikan ESP32 di tempat yang memiliki jalur pandang (line of sight) ke router.',\n '",
"Pastikan mengunci pintu kembali jika Anda sudah selesai.",
"'+jam+'  · ",
"Kelola data penghuni yang sudah disetujui,",
"';\n }\n } catch(e) {\n showToast('err', 'Respons tidak valid');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
"Edit Profile",
"Sesi Anda akan segera diakhiri. Pastikan pekerjaan Anda telah tersimpan dengan aman.",
"Mempersiapkan Kamera...",
"');\n }, 'json').fail(function() {\n showCustomAlert(\"Gagal terhubung ke API\");\n $('#chatInput').prop('disabled', false);\n submitBtn.prop('disabled', false).html('",
"Simpan'; showCustomAlert(\"Koneksi ke server gagal.\"); });\n});\n\nfunction copyCode() { var v = document.getElementById(\"hasilCodeRegister\").value, b = document.getElementById(\"btnCopy\").querySelector('i'); navigator.clipboard.writeText(v).then(function() { b.classList.replace(\"fa-copy\",\"fa-check\"); showCustomAlert('Kode Registrasi berhasil disalin!'); setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); }); }\nfunction copyRevealed() { var v = document.getElementById(\"revealedPassword\").value, b = document.getElementById(\"btnCopyRevealed\"); navigator.clipboard.writeText(v).then(function() { b.classList.replace(\"fa-copy\",\"fa-check\"); showCustomAlert('Password berhasil disalin!'); setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); }); }\n\nvar currentEditFoto = '';\n\nfunction openDeleteModal(id, name) { document.getElementById(\"delete_id_occupant\").value = id; document.getElementById(\"delete_occupant_name\").innerText = name; $(\"#modalDeleteOccupant\").modal(\"show\"); }\nfunction openEditModal(id, name, nik, no_hp, foto) {\n document.getElementById(\"edit_id\").value = id;\n document.getElementById(\"edit_name\").value = name;\n document.getElementById(\"edit_nik\").value = nik || '';\n document.getElementById(\"edit_no_hp\").value = no_hp;\n currentEditFoto = foto;\n $(\"#modalEdit\").modal(\"show\");\n}\n\nfunction generateEditFotoQr() {\n var id = document.getElementById(\"editFotoOccupantId\").value;\n document.getElementById(\"editFotoDefaultView\").style.display = 'none';\n document.getElementById(\"editFotoLoadingView\").style.display = 'block';\n var fd = new FormData();\n fd.append('id_occupant', id);\n fetch('generate_foto_token.php', { method: 'POST', body: fd })\n .then(function(r) { return r.json(); })\n .then(function(d) {\n document.getElementById(\"editFotoLoadingView\").style.display = 'none';\n if (d.status === 'success') {\n document.getElementById(\"editFotoQrView\").style.display = 'block';\n var canvas = document.getElementById(\"editFotoQrCanvas\");\n canvas.innerHTML = '';\n document.getElementById(\"editFotoLinkText\").value = d.url;\n new QRCode(canvas, {\n text: d.url,\n width: 180,\n height: 180,\n colorDark: '#1e1b4b',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n showCustomAlert(\"Gagal generate QR\");\n document.getElementById(\"editFotoDefaultView\").style.display = 'block';\n }\n }).catch(function(e) {\n document.getElementById(\"editFotoLoadingView\").style.display = 'none';\n showCustomAlert(\"Koneksi gagal saat mengambil QR.\");\n document.getElementById(\"editFotoDefaultView\").style.display = 'block';\n });\n}\nfunction copyEditFotoLink() {\n navigator.clipboard.writeText(document.getElementById('editFotoLinkText').value);\n showCustomAlert('Link Edit Foto berhasil disalin!');\n}\nfunction downloadEditFotoQr() {\n var qrCanvas = document.querySelector('#editFotoQrCanvas canvas');\n if (!qrCanvas) { showCustomAlert(\"QR Code belum ter-generate.\"); return; }\n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Perbarui_Foto.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}\nfunction closeEditFotoModal() { $(\"#modalEditFoto\").modal(\"hide\"); location.reload(); }\n\nfunction toggleStatus(id) { $.post(\"toggle_status_occupant.php\", { id_occupant: id }, function(d) { if (d.status === \"success\") location.reload(); else showCustomAlert(d.message || \"Gagal memproses.\"); }, \"json\"); }\n\n// PIN verify QR\nfunction closePinModal() {\n $(\"#modalVerifikasiPin\").modal(\"hide\");\n}\n\nfunction openPinVerifikasiModal(id) {\n document.getElementById(\"verifikasiPinOccupantId\").value = id;\n document.getElementById(\"verifikasiPinDefaultView\").style.display = \"block\";\n document.getElementById(\"verifikasiPinLoadingView\").style.display = \"none\";\n document.getElementById(\"verifikasiPinQrView\").style.display = \"none\";\n \n // Auto generate QR on open\n $(\"#modalVerifikasiPin\").modal(\"show\");\n generatePinQr();\n}\n\nfunction generatePinQr() {\n var id = document.getElementById(\"verifikasiPinOccupantId\").value;\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'none';\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'block';\n \n var fd = new FormData();\n fd.append('id_occupant', id);\n \n fetch('generate_pin_token.php', { method: 'POST', body: fd })\n .then(function(r) { return r.json(); })\n .then(function(d) {\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'none';\n if (d.status === 'success') {\n document.getElementById(\"verifikasiPinQrView\").style.display = 'block';\n document.getElementById(\"verifikasiPinLinkText\").value = d.url;\n var canvas = document.getElementById(\"verifikasiPinQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url,\n width: 200,\n height: 200,\n colorDark: '#0f172a',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n alert(\"Gagal generate QR: \" + (d.message || \"Unknown error\"));\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'block';\n }\n })\n .catch(function(e) {\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'none';\n alert(\"Terjadi kesalahan jaringan.\");\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'block';\n });\n}\n\nfunction copyPinQrLink() {\n navigator.clipboard.writeText(document.getElementById('verifikasiPinLinkText').value);\n showCustomAlert('Link verifikasi PIN berhasil disalin!');\n}\n\nfunction downloadPinQr() {\n var qrCanvas = document.querySelector('#verifikasiPinQrCanvas canvas');\n if (!qrCanvas) {\n alert(\"QR Code belum ter-generate.\");\n return;\n }\n \n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Verifikasi_PIN.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}",
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol jika ingin mengubah.",
"'+tA(bt.last_seen);try{updCh('cBT',bt);}catch(e){}}\n}\n\n// CHARTS\nfunction initCh(id,comp){const cv=document.getElementById(id);if(!cv)return;HD[id]={labels:[],ds:{}};pushD(id,comp);CH[id]=new Chart(cv.getContext('2d'),{type:'line',data:{labels:[...HD[id].labels],datasets:getDS(id,comp)},options:chO()});}\nfunction updCh(id,comp){if(!CH[id])return;pushD(id,comp);const h=HD[id];CH[id].data.labels=[...h.labels];const ds=getDS(id,comp);ds.forEach((d,i)=>{if(CH[id].data.datasets[i])CH[id].data.datasets[i].data=d.data;});CH[id].update('none');}\nfunction pushD(id,comp){const h=HD[id],e=comp.extra_data||{};const now=new Date().toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit',second:'2-digit'});h.labels.push(now);if(h.labels.length>MP)h.labels.shift();if(comp.component_key==='power_supply'){pu(h,'v',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='step_down'){pu(h,'vi',parseFloat(e.voltage_in||0));pu(h,'vo',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='battery'){pu(h,'v',parseFloat(e.voltage||0));pu(h,'p',parseFloat(e.charge_percent||0));}}\nfunction getDS(id,comp){const h=HD[id];if(comp.component_key==='power_supply')return[ld('Voltage',h.ds.v,'#d97706','rgba(217,119,6,.06)',true),ld('Current',h.ds.a,'#0891b2','rgba(8,145,178,.06)',true)];if(comp.component_key==='step_down')return[ld('V In',h.ds.vi,'#dc2626',null,false),ld('V Out',h.ds.vo,'#059669','rgba(5,150,105,.04)',true),ld('Current',h.ds.a,'#2563eb',null,false)];if(comp.component_key==='battery')return[ld('Voltage',h.ds.v,'#059669','rgba(5,150,105,.04)',true),ld('Capacity %',h.ds.p,'#d97706','rgba(217,119,6,.06)',true)];return[];}\nfunction ld(l,d,c,bg,f){return{label:l,data:[...d],borderColor:c,backgroundColor:bg||'transparent',borderWidth:1.5,fill:!!f,tension:.4,pointRadius:0,pointBackgroundColor:c};}\nfunction pu(h,k,v){if(!h.ds[k])h.ds[k]=[];h.ds[k].push(v);if(h.ds[k].length>MP)h.ds[k].shift();}\nfunction chO(){return{responsive:true,maintainAspectRatio:false,animation:false,plugins:{legend:{display:true,position:'bottom',labels:{color:'#475569',boxWidth:6,font:{size:7},padding:4}},tooltip:{mode:'index',intersect:false,backgroundColor:'#fff',titleColor:'#1e293b',bodyColor:'#475569',borderColor:'#e5e9f0',borderWidth:1,titleFont:{size:9},bodyFont:{size:8}}},scales:{x:{display:true,grid:{color:'rgba(0,0,0,.03)',drawBorder:false},ticks:{color:'#475569',font:{size:6},maxRotation:45,maxTicksLimit:6}},y:{display:true,grid:{color:'rgba(0,0,0,.04)',drawBorder:false},ticks:{color:'#475569',font:{size:7,family:'monospace'},padding:3,maxTicksLimit:5}}},interaction:{mode:'nearest',axis:'x',intersect:false}};}\n\ndocument.getElementById('bR').addEventListener('click',pull);\npull();setInterval(pull,POLL);\n})();",
"\"error\", \"message\" => \"Method tidak diizinkan\"]);\n exit;\n}\n\n// Validasi input foto\nif (!isset($_POST['foto'])) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Data foto tidak ada\"]);\n exit;\n}\n\n$fotoData = $_POST['foto'];\n$id_occupant = null;\n$token = null;\n\n// Method 1: Token-based (from QR code scan)\nif (isset($_POST['token']) && trim($_POST['token']) !== '') {\n $token = trim($_POST['token']);\n\n // Validate token\n $stmt = $pdo->prepare(\"SELECT ft.id, ft.id_occupant, ft.status FROM foto_tokens ft WHERE ft.token = ?\");\n $stmt->execute([$token]);\n $tokenRow = $stmt->fetch();\n\n if (!$tokenRow) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token tidak valid\"]);\n exit;\n }\n if ($tokenRow['status'] === 'used') {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token sudah digunakan (kadaluwarsa)\"]);\n exit;\n }\n\n $id_occupant = $tokenRow['id_occupant'];\n}\n// Method 2: Direct id_occupant (backward compat)\nelseif (isset($_POST['id_occupant']) && trim($_POST['id_occupant']) !== '') {\n $id_occupant = trim($_POST['id_occupant']);\n}\nelse {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token atau ID Occupant diperlukan\"]);\n exit;\n}\n\n// Validasi id_occupant ada di database\n$cek = $pdo->prepare(\"SELECT id_occupant, foto FROM occupant WHERE id_occupant = ?\");\n$cek->execute([$id_occupant]);\n$occupant = $cek->fetch();\n\nif (!$occupant) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Occupant tidak ditemukan\"]);\n exit;\n}\n\n// Decode base64 image\nif (preg_match('/^data:image\\/(png|jpeg|jpg|webp);base64,/', $fotoData, $matches)) {\n $ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1];\n $fotoData = preg_replace('/^data:image\\/\\w+;base64,/', '', $fotoData);\n $fotoData = base64_decode($fotoData);\n\n if ($fotoData === false) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Gagal decode gambar\"]);\n exit;\n }\n\n // Validate image size (min 50KB, max 10MB)\n $imgSize = strlen($fotoData);\n if ($imgSize",
"Daftarkan pengguna baru untuk ruangan yang tersedia,",
". Ini adalah",
"Data Berhasil Disimpan!",
"Menyimpan Sidik Jari...';\n \n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.innerHTML = '",
"— interferensi dari WiFi tetangga pada channel yang sama dapat mengganggu koneksi. Ubah ke channel 1, 6, atau 11 yang paling sedikit interferensinya.',\n '",
"\";\n }\n } else {\n $localHtml = htmlspecialchars($message); // no attachment, just text\n }\n\n if ($sender === 'occupant') {\n // Keeps occupant online\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$id_occupant]);\n }\n\n // Only proceed if there is text or an attachment\n if ($id_occupant > 0 && (!empty($message) || $hasAttachment)) {\n $stmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, reply_to_chat_id) VALUES (?, ?, ?, ?, ?)\");\n if ($stmt->execute([$id_account, $id_occupant, $sender, $localHtml, $reply_to_chat_id])) {\n $id_chat = $pdo->lastInsertId();\n \n // Fetch Telegram Reply ID if replying\n $tgReplyId = null;\n if ($reply_to_chat_id) {\n $qRep = $pdo->prepare(\"SELECT telegram_msg_id FROM chats WHERE id_chat = ? LIMIT 1\");\n $qRep->execute([$reply_to_chat_id]);\n $fRep = $qRep->fetch();\n if ($fRep && $fRep['telegram_msg_id']) {\n $tgReplyId = $fRep['telegram_msg_id'];\n }\n }\n \n // +++++ SEND TO TELEGRAM IF SENDER IS ACCOUNT +++++\n if ($sender === 'account') {\n $telStmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $telStmt->execute([$id_occupant]);\n $telegramUser = $telStmt->fetch();\n \n if ($telegramUser && !empty($telegramUser['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $website = \"https://api.telegram.org/bot\".$botToken;\n \n $postData = [\n 'chat_id' => $telegramUser['telegram_id']\n ];\n \n // IF Replying\n if ($tgReplyId) {\n $postData['reply_to_message_id'] = $tgReplyId;\n }\n \n // IF Attachment\n if ($hasAttachment) {\n $postData[$tgFileType] = new CURLFile($attachmentPath);\n if (!empty($message)) {\n $postData['caption'] = $message;\n }\n } else {\n $postData['text'] = $message;\n }\n \n $ch = curl_init($website.\"/\".$tgEndpoint);\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 30);\n $result = curl_exec($ch);\n curl_close($ch);\n \n if ($result) {\n $resJson = json_decode($result, true);\n if (isset($resJson['ok']) && $resJson['ok'] && isset($resJson['result']['message_id'])) {\n $tgMsgId = $resJson['result']['message_id'];\n $updateMsg = $pdo->prepare(\"UPDATE chats SET telegram_msg_id = ? WHERE id_chat = ?\");\n $updateMsg->execute([$tgMsgId, $id_chat]);\n }\n }\n }\n }\n // +++++++++++++++++++++++++++++++++++++++++++++++++\n \n $msgStmt = $pdo->prepare(\"SELECT * FROM chats WHERE id_chat = ?\");\n $msgStmt->execute([$id_chat]);\n $newMsg = $msgStmt->fetch();\n echo json_encode(['status' => 'success', 'data' => $newMsg]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid data']);\n exit;\n}\n\nif ($action === 'login_occupant') {\n $no_hp = isset($_POST['no_hp']) ? trim($_POST['no_hp']) : '';\n $pin = isset($_POST['pin']) ? trim($_POST['pin']) : '';\n \n if (empty($no_hp) || empty($pin)) {\n echo json_encode(['status' => 'error', 'message' => 'Lengkapi form.']);\n exit;\n }\n \n $stmt = $pdo->prepare(\"SELECT id_occupant, name, status, access FROM occupant WHERE no_hp = ?\");\n $stmt->execute([$no_hp]);\n $occ = $stmt->fetch();\n \n if ($occ) {\n if ($occ['status'] !== 'approved' || $occ['access'] !== 'active') {\n echo json_encode(['status' => 'error', 'message' => 'Akun belum disetujui atau sedang tidak aktif.']);\n exit;\n }\n \n $verifyStmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? AND pin = ?\");\n $verifyStmt->execute([$no_hp, $pin]);\n if ($verifyStmt->fetch()) {\n $_SESSION['chat_occupant_id'] = $occ['id_occupant'];\n echo json_encode(['status' => 'success', 'data' => $occ]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'No HP atau PIN salah.']);\n exit;\n}\n\nif ($action === 'delete_history') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n if ($id_occupant > 0) {\n $stmt = $pdo->prepare(\"UPDATE chats SET deleted_by_occupant = 1 WHERE id_account = ? AND id_occupant = ?\");\n $stmt->execute([$id_account, $id_occupant]);\n echo json_encode(['status' => 'success', 'message' => 'Chat history deleted from your view.']);\n exit;\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid occupant ID.']);\n exit;\n}\n\nif ($action === 'delete_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $delete_type = isset($_POST['delete_type']) ? $_POST['delete_type'] : 'me'; // 'me' or 'everyone'\n \n // Validate message belongs to account before deleting\n $stmt = $pdo->prepare(\"SELECT c.sender, c.telegram_msg_id, o.telegram_id \n FROM chats c \n LEFT JOIN occupant o ON c.id_occupant = o.id_occupant \n WHERE c.id_chat = ? AND c.id_account = ?\");\n $stmt->execute([$id_chat, $id_account]);\n $msg = $stmt->fetch();\n \n if ($msg) {\n if ($msg['sender'] === 'account') {\n // It's the admin's message\n // Delete from Database\n $delStmt = $pdo->prepare(\"DELETE FROM chats WHERE id_chat = ?\");\n $delStmt->execute([$id_chat]);\n \n // Delete from Telegram if exists AND delete_type is 'everyone'\n if ($delete_type === 'everyone' && !empty($msg['telegram_msg_id']) && !empty($msg['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $website = \"https://api.telegram.org/bot\".$botToken;\n \n $postData = [\n 'chat_id' => $msg['telegram_id'],\n 'message_id' => $msg['telegram_msg_id']\n ];\n \n $ch = curl_init($website.\"/deleteMessage\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));\n curl_setopt($ch, CURLOPT_TIMEOUT, 2);\n curl_exec($ch);\n curl_close($ch);\n }\n \n echo json_encode(['status' => 'success']);\n exit;\n } else {\n // It's the occupant's message\n // Based on user request, admin can now delete occupant messages from their own view (Delete for Me)\n // Occupant messages cannot be deleted from Telegram via bot API anyway\n \n $delStmt = $pdo->prepare(\"DELETE FROM chats WHERE id_chat = ?\");\n $delStmt->execute([$id_chat]);\n \n echo json_encode(['status' => 'success']);\n exit;\n }\n }\n \n echo json_encode(['status' => 'error', 'message' => 'Message not found']);\n exit;\n}\n\n// ── FORWARD MESSAGE ──\nif ($action === 'forward_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $target_occupant = isset($_POST['target_occupant']) ? (int)$_POST['target_occupant'] : 0;\n \n if ($id_chat > 0 && $target_occupant > 0) {\n $srcStmt = $pdo->prepare(\"SELECT message FROM chats WHERE id_chat = ?\");\n $srcStmt->execute([$id_chat]);\n $srcMsg = $srcStmt->fetch();\n \n if ($srcMsg) {\n $fwdHtml = \"",
"\"\n data-fingerok=\"",
"({x: p.x, y: p.y}));\n editLivenessScore = Math.min(100, (editLivenessFrames / 12) * 100);\n editLmFill.style.height = editLivenessScore + '%';\n if (editLivenessScore > 70) { editLmFill.style.background = '#22c55e'; editLmIcon.style.color = '#22c55e'; }\n else if (editLivenessScore > 30) { editLmFill.style.background = '#f59e0b'; editLmIcon.style.color = '#f59e0b'; }\n else { editLmFill.style.background = '#ef4444'; editLmIcon.style.color = '#ef4444'; }\n }\n if (editLivenessScore >= 50) {\n editSetStatus('ok', 'Wajah terdeteksi — Siap!');\n editBtnShutter.disabled = false;\n } else {\n editSetStatus('search', 'Mendeteksi liveness...');\n editBtnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n editSetFaceNotOk(); editSetStatus('err', 'Terdeteksi lebih dari 1 wajah');\n } else {\n editSetFaceNotOk(); editSetStatus('search', 'Posisikan wajah Anda...');\n }\n }, 300);\n }\n\n function editSetFaceNotOk() {\n editFaceOk = false;\n editOval.classList.remove('ok');\n editBtnShutter.disabled = true;\n }\n\n function startEditBrightness() {\n const tmpCanvas = document.createElement('canvas');\n const tmpCtx = tmpCanvas.getContext('2d');\n editBrightInterval = setInterval(() => {\n if (editVid.paused || editVid.ended || !editVid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(editVid, 0, 0, 80, 60);\n const data = tmpCtx.getImageData(0, 0, 80, 60).data;\n let sum = 0; const count = data.length / 4;\n for (let i = 0; i",
"Simpan kode dibawah ini untuk melakukan registrasi di Finger Print.",
"\"\n data-unread=\"",
"— tekan tombol reset atau cabut dan pasang kembali power ESP32, lalu coba hubungkan ulang.',\n '",
"Wajib Membagikan Kontak",
"(di bawah -75 dBm) untuk membuat koneksi yang stabil. Paket data handshake gagal terkirim/diterima dengan sempurna karena jarak yang terlalu jauh atau penghalang fisik antara ESP32 dan router.';\n suggestions = [\n '",
"Tambah Account",
"`;\n });\n }\n\n // Account\n if (showAccount && data.account && data.account.length > 0) {\n html += '",
"Link ini invalid atau sudah pernah digunakan. Silakan minta link terbarunya dari Admin.",
"exec(\"CREATE TABLE IF NOT EXISTS chats (\n id_chat INT AUTO_INCREMENT PRIMARY KEY,\n id_account INT NOT NULL,\n id_occupant INT NOT NULL,\n sender ENUM('account', 'occupant') NOT NULL,\n message TEXT NOT NULL,\n is_read TINYINT DEFAULT 0,\n deleted_by_occupant TINYINT DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4\");\n\n// Automatically add columns to existing tables if updating\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN deleted_by_occupant TINYINT DEFAULT 0 AFTER is_read\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN telegram_msg_id INT NULL AFTER id_chat\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN is_pinned TINYINT DEFAULT 0 AFTER reply_to_chat_id\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\n// AUTO CLEANUP: Hapus chat yang usianya lebih dari 3 hari (72 Jam)\ntry {\n $pdo->exec(\"DELETE FROM chats WHERE created_at",
"Cahaya",
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.addEventListener('DOMContentLoaded', function() {\n const usernameInput = document.getElementById('usernameInput');\n const usernameTooltip = document.getElementById('usernameTooltip');\n let checkTimeout;\n\n function checkUsername() {\n const username = usernameInput.value.trim();\n if (username === '') {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity(''); \n return;\n }\n\n const formData = new FormData();\n formData.append('username', username);\n\n fetch('check_username.php', {\n method: 'POST',\n body: formData\n })\n .then(res => res.json())\n .then(data => {\n if (data.status === 'taken') {\n usernameTooltip.style.display = 'flex';\n // Prevent form parsing if invalid\n usernameInput.setCustomValidity('Username sudah digunakan');\n } else {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity('');\n }\n })\n .catch(err => {\n console.error(err);\n showCustomAlert(\"Error\", \"Gagal memeriksa username.\", \"error\");\n });\n }\n\n usernameInput.addEventListener('input', function() {\n usernameTooltip.style.display = 'none'; // hide while typing\n clearTimeout(checkTimeout);\n checkTimeout = setTimeout(checkUsername, 500); // debounce check\n });\n\n usernameInput.addEventListener('blur', checkUsername);\n \n // Hide tooltip immediately when focusing to re-type\n usernameInput.addEventListener('focus', function() {\n usernameTooltip.style.display = 'none';\n });\n\n // Tampilkan error PHP jika ada sebagai alert juga optional (kita tampilkan sebagai error merah)",
"Edit Foto",
"Hapus",
"r.json())\n .then(res => {\n if (res.status === 'success') {\n // Update unread danger count\n let unreadCount = parseInt(res.danger_unread) || 0;\n let indicator = document.getElementById('headerWarningCountIndicator');\n let badge = document.getElementById('headerWarningBadge');\n \n if (unreadCount > 0) {\n if(indicator) indicator.style.display = 'block';\n if(badge) badge.innerText = unreadCount + ' Baru';\n } else {\n if(indicator) indicator.style.display = 'none';\n if(badge) badge.innerText = '0';\n }\n\n // Build list\n let html = '';\n let warnings = res.warnings || [];\n if (warnings.length > 0) {\n warnings.forEach(w => {\n let timeDiff = formatRelativeHeader(w.time);\n html += `",
"/forgot_password",
"email-star\" data-id=\"",
"New Password",
"`;\n hist.appendChild(tempRow);\n scrollToBottom();\n \n input.value = '';\n input.style.height = 'auto'; // Reset textarea height\n closeReplyBox();\n \n $.ajax({\n url: 'chat_api.php',\n type: 'POST',\n data: formData,\n contentType: false,\n processData: false,\n dataType: 'json',\n success: function(res) {\n if(res.status === 'success') {\n loadContacts();\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n renderMessages(res2.data);\n }\n }, 'json');\n } else {\n let el = document.getElementById(tempId);\n if (el) {\n el.querySelector('.msg-status').innerHTML = '",
"Jelaskan kegiatan yang Anda lakukan (misal: Maintenance AC)...",
"Scan ulang jaringan",
"PIN diperlukan untuk melihat password di halaman detail.",
"\\n\"\n . \"Gunakan perintah ini jika Anda melupakan PIN 6-Digit Anda. Sistem akan membuatkan sesi verifikasi wajah biometrik (FR) satu kali pakai. Setelah wajah Anda terverifikasi sesuai dengan foto pendaftaran, Anda diizinkan untuk mengatur ulang PIN Anda kembali.\\n\\n\"\n . \"🔑",
"Setup PIN - Identia Admin",
"Terjadi Kesalahan Jaringan.';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n });\n \n } catch (err) {\n statusEl.innerHTML = '",
"UNLOCK DOOR');\n \n // Animate Door Icon Back\n $('#doorIcon').removeClass('mdi-door-open').addClass('mdi-door-closed');\n }\n }\n\n function pollDoorStatus() {\n $.get('api_door_status.php', function(data) {\n if (data && data.status === 'success') {\n updateUIFromState(data.door_state);\n }\n }, 'json');\n }\n\n // Poll every 2.5 seconds\n setInterval(pollDoorStatus, 2500);\n \n // Run once immediately on load\n pollDoorStatus();\n });",
"\"\n data-danger=\"",
"Silahkan isi semua slot PIN (6 angka).",
"Password baru belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
"\" . $srcMsg['message'];\n \n $insStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message) VALUES (?, ?, 'account', ?)\");\n $insStmt->execute([$id_account, $target_occupant, $fwdHtml]);\n $newId = $pdo->lastInsertId();\n \n // Send to Telegram\n $telStmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $telStmt->execute([$target_occupant]);\n $tgUser = $telStmt->fetch();\n if ($tgUser && !empty($tgUser['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $plainText = strip_tags($srcMsg['message']);\n $postData = ['chat_id' => $tgUser['telegram_id'], 'text' => \"[Diteruskan]\\n\" . $plainText];\n $ch = curl_init(\"https://api.telegram.org/bot{$botToken}/sendMessage\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 5);\n $res = curl_exec($ch);\n curl_close($ch);\n if ($res) {\n $rj = json_decode($res, true);\n if (isset($rj['ok']) && $rj['ok'] && isset($rj['result']['message_id'])) {\n $pdo->prepare(\"UPDATE chats SET telegram_msg_id = ? WHERE id_chat = ?\")->execute([$rj['result']['message_id'], $newId]);\n }\n }\n }\n echo json_encode(['status' => 'success']);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid forward data']);\n exit;\n}\n\n// ── EDIT MESSAGE ──\nif ($action === 'edit_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $new_message = isset($_POST['new_message']) ? trim($_POST['new_message']) : '';\n \n if ($id_chat > 0 && !empty($new_message)) {\n // Only allow editing account messages\n $stmt = $pdo->prepare(\"SELECT sender, telegram_msg_id, (SELECT telegram_id FROM occupant WHERE id_occupant = chats.id_occupant) as telegram_id FROM chats WHERE id_chat = ? AND id_account = ?\");\n $stmt->execute([$id_chat, $id_account]);\n $msg = $stmt->fetch();\n \n if ($msg && $msg['sender'] === 'account') {\n $formattedHtml = htmlspecialchars($new_message) . \"",
"Menunggu Sensor...';\n statusEl.className = \"mt-4 text-info font-weight-bold\";\n statusEl.style.display = \"block\";\n \n if (!window.PublicKeyCredential) {\n statusEl.innerHTML = '",
"\"\n data-detail=\"",
"prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n }\n }\n unset($tokens[$postToken]);\n file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));\n }\n }\n echo json_encode(['status'=>'ok']);\n exit;\n }\n }\n\n // Validasi token via local request atau include logic langsung\n // Karena ini di file yang sama (local), kita bisa request HTTP ke server sendiri\n // Tapi untuk mudahnya karena sudah ada db:\n $manifestPath = __DIR__ . '/manifest_pin_tokens.json';\n if (file_exists($manifestPath)) {\n $json = file_get_contents($manifestPath);\n $tokens = json_decode($json, true) ?: [];\n \n if (isset($tokens[$tokenParam])) {\n $tokenData = $tokens[$tokenParam];\n if (time()",
"— nama WiFi harus diketik persis sama termasuk huruf besar/kecil. Contoh: \"WiFi_Rumah\" ≠ \"wifi_rumah\" ≠ \"WiFi Rumah\".',\n '",
"\";\n \n // Update local DB\n $updStmt = $pdo->prepare(\"UPDATE chats SET message = ? WHERE id_chat = ?\");\n $updStmt->execute([$formattedHtml, $id_chat]);\n \n // Update Telegram if telegram_msg_id exists\n if (!empty($msg['telegram_msg_id']) && !empty($msg['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $postData = [\n 'chat_id' => $msg['telegram_id'],\n 'message_id' => $msg['telegram_msg_id'],\n 'text' => $new_message\n ];\n $ch = curl_init(\"https://api.telegram.org/bot{$botToken}/editMessageText\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 5);\n curl_exec($ch);\n curl_close($ch);\n }\n echo json_encode(['status' => 'success', 'new_message' => $formattedHtml]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Cannot edit this message']);\n exit;\n}\n\n// ── BULK DELETE MESSAGES ──\nif ($action === 'bulk_delete_messages') {\n $ids = isset($_POST['id_chats']) ? $_POST['id_chats'] : [];\n $deleteType = isset($_POST['delete_type']) ? $_POST['delete_type'] : 'me'; // 'everyone' or 'me'\n \n if (is_array($ids) && count($ids) > 0) {\n $deletedCount = 0;\n foreach ($ids as $mid) {\n $mid = (int)$mid;\n if ($mid",
"Foto Profil (Opsional)",
"Simpan'; \n showCustomAlert(\"Koneksi ke server gagal.\"); \n });\n });\n\n $(\"#formDeleteAccount\").submit(function(e) { \n e.preventDefault(); \n var val = $(\"#delete_id_account\").val();\n if(val === \"BULK\") {\n $(\"#btnDeleteAccount\").prop(\"disabled\",true);\n $(\"#btnDeleteAccount\").html('",
"Ingat password?",
"Identity data not found.",
"Link (tautan) tidak valid atau sesi kadaluwarsa. Silakan minta link terbarunya dari Admin.",
"Simpan';\n if (d.status === 'success') {\n $('#' + modalId).modal('hide');\n if (typeof showCustomAlert === 'function') showCustomAlert('Berhasil', d.message, 'success');\n setTimeout(() => location.reload(), 1000);\n } else {\n if (typeof showCustomAlert === 'function') showCustomAlert('Gagal', d.message, 'error');\n else alert(d.message);\n }\n }).catch(() => {\n btn.disabled = false;\n btn.innerHTML = '",
"\";\n $postData = ['chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'HTML'];\n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n \n // Pin pesan Panduan Penggunaan\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $helpMsgId = $resData['result']['message_id'];\n file_get_contents($website.\"/pinChatMessage?chat_id=$chatId&message_id=$helpMsgId\");\n }\n exit;\n }\n \n if ($textMessage === '/forgot_pin') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_pin_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/verifikasi_wajah.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Biometric Verification', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Silakan klik tombol di bawah ini untuk memulai verifikasi wajah.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
"Silakan klik foto di bawah ini untuk memperbarui foto. Jika",
"Daftar Notifikasi Terbaru",
"';\n });\n }\n\n // Input event with debounce\n searchInput.addEventListener('input', function() {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n doSearch(this.value.trim());\n }, 300);\n });\n\n // Focus shows dropdown if there's content\n searchInput.addEventListener('focus', function() {\n if (this.value.trim().length >= 1) {\n doSearch(this.value.trim());\n }\n });\n\n // Tab clicks\n searchTabs.addEventListener('click', function(e) {\n const btn = e.target.closest('.search-tab-item');\n if (!btn) return;\n searchTabs.querySelectorAll('.search-tab-item').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n activeTab = btn.getAttribute('data-tab');\n const query = searchInput.value.trim();\n if (query) {\n renderResults(cachedData, query, activeTab);\n }\n });\n\n // Close on Escape\n document.addEventListener('keydown', function(e) {\n if (e.key === 'Escape') {\n searchDropdown.style.display = 'none';\n }\n });\n\n // Close on click outside\n document.addEventListener('click', function(e) {\n if (!searchDropdown.contains(e.target) && e.target !== searchInput && !e.target.closest('#navbar-search-icon')) {\n searchDropdown.style.display = 'none';\n }\n });\n})();",
"\");\n}\n\n$id_occupant = $tokenData['id_occupant'];\n\n// Ambil data occupant\n$stmt = $pdo->prepare(\"SELECT name, nik_nip, no_hp, foto FROM occupant WHERE id_occupant = ?\");\n$stmt->execute([$id_occupant]);\n$occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n\nif (!$occupant) {\n die(\"",
"Hapus Permanen",
"Lihat",
"Hari Ini",
"\\n\\n\"\n . \"Berikut adalah rincian fitur mandiri yang dapat Anda akses menggunakan Telegram Web App secara instan dan aman:\\n\\n\"\n . \"🔐",
"`;}\n if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';\n h+=`",
"document.addEventListener('DOMContentLoaded', () => {\n const group = document.getElementById('pinGroup');\n const inputs = group.querySelectorAll('.pin-box');\n const hiddenInput = document.getElementById('realPin');\n\n inputs.forEach((input, index) => {\n input.addEventListener('input', (e) => {\n input.value = input.value.replace(/\\D/g, '');\n if (input.value !== '' && index",
"Token QR code ini sudah pernah digunakan.",
".content-wrapper, .footer { background: #f8f9fb !important; }\n/* Perkecil wrapper DataTables */\n.dataTables_wrapper .dataTables_length,\n.dataTables_wrapper .dataTables_filter,\n.dataTables_wrapper .dataTables_info,\n.dataTables_wrapper .dataTables_paginate {\n font-size: 0.875rem;\n}\n\n/* Dropdown Show entries lebih kecil */\n.dataTables_wrapper .dataTables_length select {\n padding: 0.15rem 0.1rem 0.15rem 0.4rem; /* padding kanan dikurangi */\n font-size: 0.8rem;\n height: 28px;\n line-height: 1.2;\n width: auto; /* menyesuaikan konten */\n min-width: 40px; /* batas minimal */\n max-width: 60px; /* batas maksimal agar tidak melebar */\n background: transparent !important;\n border: 0px solid #ddd !important;\n color: #333 !important;\n margin: 0 4px;\n border-radius: 4px;\n}\n\n/* Input pencarian lebih kecil */\n.dataTables_wrapper .dataTables_filter input {\n padding: 0.15rem 0.5rem;\n font-size: 0.8rem;\n height: 28px;\n}\n/* Pagination: hilangkan background, hanya border, dan beri jarak */\n/* Jarak antara tabel dan info/pagination */\n.dataTables_wrapper .dataTables_info,\n.dataTables_wrapper .dataTables_paginate {\n margin-top: 1rem !important;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button.current {\n background: transparent !important;\n border: 1px solid #ddd !important;\n color: #7b7979 !important;\n margin: 0 4px;\n border-radius: 4px;\n}\n\n/* Tombol pagination */\n.dataTables_wrapper .dataTables_paginate .paginate_button {\n padding: 0.25rem 0.6rem;\n font-size: 0.875rem;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button.previous,\n.dataTables_wrapper .dataTables_paginate .paginate_button.next {\n padding: 0.25rem 0.75rem;\n}\n\ntable#myTable td,\ntable#myTable th {\n padding: 1rem 1.5rem !important;\n}\ntable#myTable,\ntable#myTable th,\ntable#myTable td {\n border: none !important;\n}\n\n/* Pastikan tidak ada border bawah pada tabel */\ntable#myTable {\n border-bottom: none !important;\n margin-bottom: 0.5rem;\n}\n\n/* Hapus border dari pembungkus tabel */\n.table-responsive {\n border-bottom: none;\n}\n\n/* Antisipasi garis dari wrapper DataTables */\n.dataTables_wrapper {\n border-top: none;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button:hover {\n background: #f5f6f8 !important; /* abu putih soft */\n border: 1px solid #ddd !important;\n color: #333 !important;\n border-radius: 4px;\n}\n\n.dataTables_wrapper .dataTables_paginate .paginate_button:focus {\n box-shadow: none !important;\n}\n\n/* INPUT SEARCH DATATABLES */\n.dataTables_wrapper .dataTables_filter input {\n background: transparent !important; /* transparan */\n border: 1px solid #d1d5db !important; /* abu soft */\n border-radius: 3px;\n color: #333 !important;\n padding: 0.3rem 0.6rem;\n transition: 0.2s ease;\n}\n\n/* Saat focus */\n.dataTables_wrapper .dataTables_filter input:focus {\n outline: none !important;\n border: 1px solid #60a5fa !important; /* biru soft */\n box-shadow: 0 0 0 2px rgba(96,165,250,0.15);\n}\n/* Untuk Chrome, Edge, Safari */\n.dataTables_wrapper .dataTables_filter input[type=\"search\"]::-webkit-search-cancel-button {\n -webkit-appearance: none;\n height: 14px;\n width: 14px;\n cursor: pointer;\n background: none;\n position: relative;\n}\n\n/* Buat custom X warna hitam */\n.dataTables_wrapper .dataTables_filter input[type=\"search\"]::-webkit-search-cancel-button:after {\n content: \"✕\";\n color: #1e2223; /* HITAM */\n font-size: 14px;\n font-weight: bold;\n}\ntable.dataTable tbody tr td {\n vertical-align: middle;\n}\n\n/* Warna Striped Tabel Selang-Seling Konsisten */\ntable.dataTable tbody tr.odd > td,\ntable#myTable tbody tr.row-odd > td {\n background-color: #fafafa !important;\n}\ntable.dataTable tbody tr.even > td,\ntable#myTable tbody tr.row-even > td {\n background-color: #ffffff !important;\n}\n\n/* KETIKA DROPDOWN DIBUKA: Induk (Menu) WAJIB Putih, Anak (Dropdown) WAJIB Abu-abu */\ntable.dataTable tbody tr.odd.shown > td,\ntable.dataTable tbody tr.even.shown > td,\ntable#myTable tbody tr.shown > td {\n background-color: #ffffff !important;\n}\n\ntable.dataTable tbody tr.shown + tr > td,\ntable#myTable tbody tr.shown + tr > td {\n background-color: #fafafa !important;\n}\n\n/* MENGGESER SELURUH BARIS KE BAWAH/ATAS AGAR SELANG-SELING TIDAK TERPUTUS. \n Kasus 1: Menu Genap dibuka -> Seluruh menu di BAWAHNYA harus bertukar warna. */\ntable.dataTable tbody tr.even.shown ~ tr.odd > td,\ntable#myTable tbody tr.row-even.shown ~ tr.row-odd > td {\n background-color: #ffffff !important;\n}\ntable.dataTable tbody tr.even.shown ~ tr.even > td,\ntable#myTable tbody tr.row-even.shown ~ tr.row-even > td {\n background-color: #fafafa !important;\n}\n\n/* Kasus 2: Menu Ganjil dibuka -> Karena ia berubah jadi Putih, maka seluruh menu di ATASNYA harus bertukar warna! \n (Memerlukan CSS :has() selector untuk menarget elemen sebelum tr.shown) */\ntable.dataTable tbody:has(tr.odd.shown) tr.even:not(.shown ~ tr) > td,\ntable#myTable tbody:has(tr.row-odd.shown) tr.row-even:not(.shown ~ tr) > td {\n background-color: #fafafa !important;\n}\ntable.dataTable tbody:has(tr.odd.shown) tr.odd:not(.shown):not(.shown ~ tr) > td,\ntable#myTable tbody:has(tr.row-odd.shown) tr.row-odd:not(.shown):not(.shown ~ tr) > td {\n background-color: #ffffff !important;\n}\n\ntable.dataTable tbody tr.shown + tr td {\n padding: 0 !important;\n border: none !important;\n}",
"Silakan mendaftarkan sidik jari untuk akun ini.",
"Token Tidak Valid",
"// ===== TFT Component-Based UI Builder =====\nconst COMP_DEFAULTS = {\n sidebar: {w:80,h:320,bg:'#1a1a2e',border:0,borderColor:'#333',radius:0,label:'Sidebar'},\n header: {w:480,h:40,bg:'#16213e',border:0,borderColor:'#333',radius:0,label:'Header'},\n card: {w:140,h:100,bg:'#1e1e30',border:1,borderColor:'#444',radius:8,label:'Card'},\n button: {w:90,h:32,bg:'#4B49AC',border:0,borderColor:'#666',radius:6,label:'Button',text:'Button',textColor:'#FFFFFF',fontSize:12,textAlign:'center'},\n toggle: {w:44,h:22,bg:'#34C759',border:0,borderColor:'transparent',radius:11,label:'Toggle',toggleState:true},\n progressbar: {w:120,h:10,bg:'#333',border:0,borderColor:'transparent',radius:5,label:'Progress',value:65,barColor:'#4B49AC'},\n datavalue: {w:80,h:36,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'DataValue',text:'24.5°C',textColor:'#00FF88',fontSize:18,textAlign:'center'},\n label: {w:100,h:20,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Label',text:'Label Text',textColor:'#CCCCCC',fontSize:12,textAlign:'left'},\n icon: {w:28,h:28,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Icon',icon:'★',textColor:'#FFD700',fontSize:18,textAlign:'center'}\n};\n\nconst CONTAINERS = ['sidebar','header','card'];\n\nclass TFTBuilder {\n constructor(){\n this.screens=[{name:'Screen 1',components:[]}];\n this.currentScreen=0;\n this.selectedId=null;\n this.idCounter=1;\n this.history=[];\n this.historyIdx=-1;\n this.designId=0;\n this.orientation='landscape';\n this.init();\n }\n\n init(){\n this.bindDragDrop();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.pushHistory();\n this.loadList();\n document.addEventListener('keydown',e=>{\n if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;\n if(e.key==='Delete'||e.key==='Backspace')this.deleteSelected();\n if(e.ctrlKey&&e.key==='z'){e.preventDefault();this.undo();}\n if(e.ctrlKey&&e.key==='y'){e.preventDefault();this.redo();}\n });\n }\n\n // ===== ORIENTATION =====\n setOrientation(o){\n this.orientation=o;\n const f=document.getElementById('canvasFrame');\n if(o==='landscape'){f.style.width='480px';f.style.height='320px';}\n else{f.style.width='320px';f.style.height='480px';}\n this.renderCanvas();\n }\n getW(){return this.orientation==='landscape'?480:320;}\n getH(){return this.orientation==='landscape'?320:480;}\n\n // ===== DRAG & DROP FROM LIBRARY =====\n bindDragDrop(){\n document.querySelectorAll('.clib-item').forEach(el=>{\n el.addEventListener('dragstart',e=>{\n e.dataTransfer.setData('newComp',el.dataset.comp);\n e.dataTransfer.effectAllowed='copy';\n });\n });\n const canvas=document.getElementById('tftCanvas');\n canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});\n canvas.addEventListener('drop',e=>{\n e.preventDefault();\n const compType=e.dataTransfer.getData('newComp');\n if(!compType)return;\n const rect=canvas.getBoundingClientRect();\n const x=Math.max(0,Math.round((e.clientX-rect.left)/5)*5);\n const y=Math.max(0,Math.round((e.clientY-rect.top)/5)*5);\n // Check if dropped on a container\n let parentId=null;\n const target=document.elementFromPoint(e.clientX,e.clientY);\n if(target){\n const parentEl=target.closest('.tft-comp[data-container=\"true\"]');\n if(parentEl)parentId=parentEl.dataset.id;\n }\n this.addComponent(compType,x,y,parentId);\n });\n canvas.addEventListener('click',e=>{\n if(e.target===canvas||e.target.id==='tftCanvas'){this.select(null);}\n });\n }\n\n // ===== COMPONENT MANAGEMENT =====\n addComponent(type,x=10,y=10,parentId=null){\n const d=JSON.parse(JSON.stringify(COMP_DEFAULTS[type]));\n const id='c'+this.idCounter++;\n const comp={id,type,x,y,...d,padding:4,children:[],interactions:{event:'none',action:'none',target:'',payload:''}};\n if(parentId){\n const parent=this.findComp(this.currentComps(),parentId);\n if(parent){\n // Position relative to parent\n const pr=this.getAbsPos(parentId);\n comp.x=Math.max(0,x-(pr?.x||0));\n comp.y=Math.max(0,y-(pr?.y||0));\n parent.children.push(comp);\n }else{this.currentComps().push(comp);}\n }else{this.currentComps().push(comp);}\n this.pushHistory();\n this.renderCanvas();\n this.select(id);\n return id;\n }\n\n currentComps(){return this.screens[this.currentScreen].components;}\n\n findComp(arr,id){\n for(const c of arr){\n if(c.id===id)return c;\n if(c.children){const f=this.findComp(c.children,id);if(f)return f;}\n }\n return null;\n }\n\n findParent(arr,id,parent=null){\n for(const c of arr){\n if(c.id===id)return{parent,list:arr};\n if(c.children){const f=this.findParent(c.children,id,c);if(f)return f;}\n }\n return null;\n }\n\n removeComp(id){\n const info=this.findParent(this.currentComps(),id);\n if(!info)return;\n const idx=info.list.findIndex(c=>c.id===id);\n if(idx>=0)info.list.splice(idx,1);\n }\n\n getAbsPos(id){\n // Walk up hierarchy to get absolute position\n const walk=(arr,tx,ty)=>{\n for(const c of arr){\n const ax=tx+c.x, ay=ty+c.y;\n if(c.id===id)return{x:ax,y:ay};\n if(c.children){const r=walk(c.children,ax,ay);if(r)return r;}\n }\n return null;\n };\n return walk(this.currentComps(),0,0);\n }\n\n deleteSelected(){\n if(!this.selectedId)return;\n this.removeComp(this.selectedId);\n this.selectedId=null;\n this.pushHistory();\n this.renderCanvas();\n this.renderHierarchy();\n this.clearProps();\n }\n\n // ===== SELECTION =====\n select(id){\n this.selectedId=id;\n document.querySelectorAll('.tft-comp').forEach(el=>el.classList.remove('selected'));\n if(id){\n const el=document.querySelector(`.tft-comp[data-id=\"${id}\"]`);\n if(el)el.classList.add('selected');\n this.renderProps();\n }else{this.clearProps();}\n this.renderHierarchy();\n }\n\n clearProps(){\n document.getElementById('propsContent').innerHTML='",
"Silakan klik 'X' untuk menutup jendela.",
".\\n\\n\"\n . \"Silakan sampaikan pertanyaan, laporan kendala, atau pesan Anda di sini kapan pun Anda butuhkan. Admin akan membalas pesan Anda dari Control Panel sesegera mungkin.\";\n \n $removeKeyboard = ['remove_keyboard' => true];\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($removeKeyboard)\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n } else {\n // Nomor tidak terdaftar\n $text = \"Maaf, nomor handphone Anda ($phoneNumber) tidak ditemukan pada daftar Penghuni Identia.\\n\\nPastikan Anda mendaftar melalui Admin terlebih dahulu.\";\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n }\n exit;\n }\n \n // Check if the message contains media\n if (isset($update[\"message\"][\"photo\"]) || \n isset($update[\"message\"][\"document\"]) || \n isset($update[\"message\"][\"voice\"]) || \n isset($update[\"message\"][\"audio\"]) || \n isset($update[\"message\"][\"video\"]) ||\n isset($update[\"message\"][\"sticker\"]) ||\n isset($update[\"message\"][\"animation\"])) {\n \n $messageId = $update[\"message\"][\"message_id\"];\n \n // Hapus pesan media yang dikirimkan user tersebut langsung dari Telegram\n $deleteData = [\n 'chat_id' => $chatId,\n 'message_id' => $messageId\n ];\n file_get_contents($website.\"/deleteMessage?\".http_build_query($deleteData));\n\n // Kirim pesan peringatan ke user\n $text = \"⚠️",
"Waktu akses (5 menit) telah habis. Silakan gunakan perintah /forgot_password di Telegram lagi.",
"');\n \n if(response.status === 'success') {\n originalAccessValues[idOccupant] = accessValue; // Update original value\n showCustomAlert('Access berhasil diperbarui menjadi ' + accessValue);\n \n // Visual feedback on row\n var row = selectElement.closest('tr');\n row.css('background-color', '#ecfdf5');\n setTimeout(function() {\n row.css('background-color', '');\n }, 1000);\n } else {\n showCustomAlert(response.message || 'Gagal memperbarui access.');\n selectElement.val(originalAccessValues[idOccupant]); // Revert\n btn.prop('disabled', true);\n }\n },\n error: function() {\n selectElement.prop('disabled', false);\n selectElement.val(originalAccessValues[idOccupant]); // Revert\n btn.prop('disabled', true).html('",
"gagal menemukan jaringan WiFi dengan nama \"' + targetSsid + '\"",
"Detection Error';\n console.error('Face detection error:', err);\n });\n }\n\n document.getElementById('registerForm').addEventListener('submit', function(e) {\n if (document.getElementById('faceVerifiedFlag').value !== '1') {\n e.preventDefault();\n showCustomAlert('Foto Belum Diverifikasi', 'Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.', 'error');\n return;\n }\n var pass = document.getElementById(\"register_password\").value;\n const pwValid = (pass.length >= 8 && /[a-z]/.test(pass) && /[A-Z]/.test(pass) && /\\d/.test(pass) && /[^A-Za-z0-9]/.test(pass));\n if (!pwValid) {\n e.preventDefault();\n showCustomAlert(\"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"register_password\").focus();\n }\n });\n\n document.getElementById('register_password').addEventListener('input', function() {\n var pass = this.value;\n var meter = document.getElementById('pwStrengthMeterReg');\n var bar = document.getElementById('pwStrengthBarReg');\n var text = document.getElementById('pwStrengthTextReg');\n\n if(pass.length > 0) {\n meter.style.display = 'flex';\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.style.width = '100%'; bar.style.backgroundColor = '#10b981'; // Kuat\n text.innerText = 'Password Kuat'; text.style.color = '#10b981';\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.style.width = '66%'; bar.style.backgroundColor = '#f59e0b'; // Sedang\n text.innerText = 'Password Sedang'; text.style.color = '#f59e0b';\n } else {\n bar.style.width = '33%'; bar.style.backgroundColor = '#ef4444'; // Lemah\n text.innerText = 'Password Lemah'; text.style.color = '#ef4444';\n }\n } else {\n meter.style.display = 'none';\n text.innerText = '';\n }\n });\n\n // Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n });",
"' + msg;\n toast.style.display = 'flex';\n }\n\n function setProgress(pct) {\n if (loadProgress) loadProgress.style.width = Math.min(100, pct) + '%';\n }\n\n function updateSecChip(el, active) {\n if (!el) return;\n el.classList.remove('active','pending');\n el.classList.add(active ? 'active' : 'pending');\n }\n\n // ── Anti-screenshot / right-click ──\n document.addEventListener('contextmenu', function(e) { e.preventDefault(); });\n\n // ── Step 1: Validate Token ──\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n startTokenCheck();\n } else {\n document.getElementById('errTitle').textContent =\n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Token Sudah Digunakan' : 'Token Tidak Valid';\n document.getElementById('errMsg').textContent = d.message || 'Token tidak valid.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Gagal';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n // ── Periodic token validity check ──\n function startTokenCheck() {\n tokenCheckInterval = setInterval(function() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (!d.valid && !sessionSaved) {\n clearInterval(tokenCheckInterval);\n stopEverything();\n document.getElementById('errTitle').textContent = 'Token Kadaluwarsa';\n document.getElementById('errMsg').textContent = d.message || 'Token sudah tidak valid.';\n showPage(pgError);\n }\n } catch(e) {}\n };\n xhr.send();\n }, 30000);\n }\n\n function stopEverything() {\n clearInterval(detectLoop);\n clearInterval(brightInterval);\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n }\n\n // ── Step 2: Load models + camera ──\n var modelLoaded = false, landmarkLoaded = false, cameraStarted = false;\n\n function initCamera() {\n setProgress(5);\n loadText.textContent = 'Memuat library deteksi wajah...';\n\n function waitForLib(attempt) {\n if (attempt > 150) { // ~30s\n showLibError();\n return;\n }\n if (window._faceApiLoadFailed) {\n showLibError();\n return;\n }\n if (typeof faceapi === 'undefined') {\n setTimeout(function() { waitForLib(attempt + 1); }, 200);\n return;\n }\n setProgress(20);\n loadText.textContent = 'Library dimuat. Memulai kamera...';\n startAll();\n }\n\n function showLibError() {\n camLoading.style.display = 'none';\n document.getElementById('errTitle').textContent = 'Gagal Memuat Library';\n document.getElementById('errMsg').textContent = 'Library deteksi wajah tidak dapat dimuat. Periksa koneksi internet Anda dan coba lagi.';\n document.getElementById('errIconWrap').className = 'err-icon invalid';\n document.getElementById('errIcon').className = 'mdi mdi-cloud-off-outline';\n var retryBtn = document.getElementById('btnRetryLib');\n if (retryBtn) retryBtn.style.display = 'inline-flex';\n showPage(pgError);\n }\n\n function startAll() {\n var camPromise = startCamera();\n setProgress(30);\n\n // Load TinyFaceDetector\n loadText.textContent = 'Memuat model deteksi wajah...';\n loadModels(0);\n\n camPromise.then(function(ok) {\n cameraStarted = ok;\n setProgress(50);\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n function loadModels(urlIdx) {\n if (urlIdx >= MODEL_URLS.length) {\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat model deteksi wajah');\n return;\n }\n var url = MODEL_URLS[urlIdx];\n Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(url),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(url)\n ]).then(function() {\n modelLoaded = true;\n landmarkLoaded = true;\n setProgress(80);\n loadText.textContent = 'Model siap!';\n onReadyCheck();\n }).catch(function() {\n loadModels(urlIdx + 1);\n });\n }\n\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n setProgress(100);\n setTimeout(function() {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n setTimeout(function() {\n if (instrPanel) instrPanel.style.opacity = '0';\n setTimeout(function() { if (instrPanel) instrPanel.style.display = 'none'; }, 500);\n }, 6000);\n }, 400);\n }\n }\n\n waitForLib(0);\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
"'error', 'message' => 'Invalid ID']);\n exit;\n }\n $stmt = $pdo->prepare(\"SELECT * FROM tft_designs WHERE id = ?\");\n $stmt->execute([$id]);\n $row = $stmt->fetch();\n if ($row) {\n echo json_encode(['status' => 'success', 'data' => $row]);\n } else {\n echo json_encode(['status' => 'error', 'message' => 'Design not found']);\n }\n break;\n\n // ========== LIST ALL DESIGNS ==========\n case 'list':\n $stmt = $pdo->query(\"SELECT id, name, version_hash, created_at, updated_at FROM tft_designs ORDER BY updated_at DESC\");\n $rows = $stmt->fetchAll();\n echo json_encode(['status' => 'success', 'data' => $rows]);\n break;\n\n // ========== DELETE DESIGN ==========\n case 'delete':\n $id = intval($_POST['id'] ?? 0);\n if ($id",
"Factory reset jika berulang",
"PIN diperlukan untuk melihat password di halaman operations.",
"\"\n data-no_hp=\"",
"WhatsApp belum diisi",
"— jarak terlalu jauh atau terhalang dinding tebal dapat menyebabkan sinyal tidak terdeteksi. Idealnya dalam jarak",
"dengan saluran pribadi Admin",
"'+tA(tf.last_seen);}\n if(bz){const e=bz.extra_data||{};el('info-bz').innerHTML=mr('mdi-music-note','Tone',e.last_tone||'None','c')+mr('mdi-counter','Beeps',e.beep_count||'0','b');el('ft-bz').innerHTML='",
"Forgot password?",
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.getElementById('loginFinger').addEventListener('click', async () => {\n try {\n if (!window.PublicKeyCredential) {\n showCustomAlert(\"Not Supported\", \"This browser does not support WebAuthn (Passkey / Fingerprint).\", \"error\");\n return;\n }\n\n const publicKey = {\n challenge: new Uint8Array(32),\n rpId: window.location.hostname,\n timeout: 60000,\n userVerification: \"required\"\n };\n\n const assertion = await navigator.credentials.get({ publicKey });\n if(assertion) {\n const credId = btoa(String.fromCharCode.apply(null, new Uint8Array(assertion.rawId)));\n \n const formData = new FormData();\n formData.append('credential_id', credId);\n\n const res = await fetch('verify_finger_login.php', {\n method: 'POST',\n body: formData\n });\n const result = await res.json();\n\n if (result.status === 'success') {\n showCustomAlert(\"Success\", \"Fingerprint login successful.\", \"success\");\n setTimeout(() => { window.location.href = 'dashboard.php'; }, 1000);\n } else {\n showCustomAlert(\"Access Denied\", result.message, \"error\");\n }\n }\n } catch(err) {\n console.error(err);\n // showCustomAlert(\"Cancelled\", \"Authentication cancelled or fingerprint not recognized.\", \"warning\");\n }\n });\n\n // Tampilkan error PHP jika ada menggunakan custom alert",
"saat melakukan scanning frekuensi 2.4GHz. Router mungkin sedang mati, berada di luar jangkauan, atau SSID dalam mode tersembunyi (hidden).' ;\n suggestions = [\n '",
"Bagikan QR code ini ke",
"Facebook belum diisi",
"0.6;\n\n // Face center validation\n var cx = box.x + box.width / 2;\n var cy = box.y + box.height / 2;\n var dx = Math.abs(cx - vw / 2) / vw;\n var dy = Math.abs(cy - vh / 2) / vh;\n var faceNotCenter = dx > 0.2 || dy > 0.2;\n\n if (faceTooSmall) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu jauh — Dekatkan');\n updateSecChip(secFace, false);\n } else if (faceTooBig) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu dekat — Mundur');\n updateSecChip(secFace, false);\n } else if (faceNotCenter) {\n setFaceNotOk();\n setStatus('err', 'Pusatkan wajah dalam bingkai');\n updateSecChip(secFace, false);\n } else {\n // Face is good\n faceOk = true;\n oval.classList.add('ok');\n corners.classList.add('ok');\n scanbar.classList.add('ok');\n updateSecChip(secFace, true);\n\n // Liveness: track landmark movement\n if (landmarks && landmarkLoaded) {\n var pts = landmarks.positions;\n if (prevLandmarks && pts.length === prevLandmarks.length) {\n var totalDiff = 0;\n for (var i = 0; i",
"/* ========================================================= */\n /* CUSTOM LOGOUT MODAL STYLING */\n /* ========================================================= */\n .custom-modal-logout {\n border-radius: 10px !important;\n box-shadow: none !important;\n overflow: hidden;\n background-color: #ffffff;\n }\n .custom-modal-logout .modal-body {\n position: relative;\n z-index: 1;\n }\n /* Hiasan background abstrak */\n .custom-modal-logout::before {\n content: \"\";\n position: absolute;\n top: -60px;\n left: -40px;\n width: 180px;\n height: 180px;\n background: rgba(255, 255, 255, 0.04);\n border-radius: 50%;\n z-index: 0;\n }\n .custom-modal-logout::after {\n content: \"\";\n position: absolute;\n bottom: -50px;\n right: -50px;\n width: 140px;\n height: 140px;\n background: rgba(255, 71, 71, 0.04);\n border-radius: 50%;\n z-index: 0;\n }\n \n .modal-icon-container {\n position: relative;\n width: 85px;\n height: 85px;\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n }\n .modal-icon-container .icon-circle {\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, #ff0000 0%, #ff4d4d 100%);\n border-radius: 50%;\n display: flex;\n justify-content: center;\n align-items: center;\n box-shadow: 0 8px 25px rgba(255, 0, 0, 0.5);\n animation: pulse-soft 2s infinite;\n }\n .modal-icon-container i {\n font-size: 36px;\n color: #ffffff;\n font-weight: bold;\n }\n .modal-title-custom {\n font-weight: 700;\n color: #2c2e33;\n font-size: 1.4rem;\n z-index: 2;\n position: relative;\n }\n .modal-desc-custom {\n font-size: 0.95rem;\n line-height: 1.6;\n z-index: 2;\n position: relative;\n padding: 0 10px;\n }\n .btn-cancel-custom {\n border-radius: 10px !important;\n font-weight: 600;\n padding: 10px 24px;\n color: #6c7383;\n background-color: #f4f5f7;\n border: 1px solid #e1e3e9;\n transition: all 0.3s ease;\n z-index: 2;\n position: relative;\n display: inline-flex;\n align-items: center;\n }\n .btn-cancel-custom:hover {\n background-color: #e4e6ea;\n transform: translateY(-2px);\n }\n .btn-logout-custom {\n border-radius: 10px !important;\n font-weight: 600;\n padding: 10px 26px;\n background: linear-gradient(135deg, #4B49AC 0%, #4B49AC 100%);\n border: none;\n transition: all 0.3s ease;\n z-index: 2;\n position: relative;\n color: #ffffff !important;\n display: inline-flex;\n align-items: center;\n }\n .btn-logout-custom:hover {\n background: linear-gradient(135deg, #4B49AC 0%, #4B49AC 100%);\n transform: translateY(-2px);\n color: #ffffff !important;\n }\n \n @keyframes pulse-soft {\n 0% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.5); }\n 70% { box-shadow: 0 0 0 20px rgba(255, 107, 107, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0); }\n }\n /* Mengubah border radius semua button dengan class .btn */\n .btn {\n border-radius: 4px !important;\n }\n .btn-primary {\n border-radius: 6px !important;\n }\n .navbar {\n -webkit-box-shadow: none !important;\n -moz-box-shadow: none !important;\n box-shadow: none !important;\n border-bottom: 1px solid #ebedf2;\n }\n .sidebar {\n border-right: 1px solid #ebedf2;\n }\n .sidebar .nav {\n padding-top: 5px !important;\n margin-top: 5px !important;\n }\n .sidebar .nav > .nav-item:first-child {\n margin-top: 5px !important;\n padding-top: 5px !important;\n }\n .sidebar .nav .nav-item .nav-link {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n }\n .sidebar .nav .nav-item .nav-link i.menu-icon {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n width: 20px !important;\n min-width: 20px !important;\n }\n .sidebar .nav .nav-item .nav-link .menu-title {\n color: #504e55ff !important;\n line-height: 1 !important;\n }\n .sidebar .nav .nav-item .nav-link i.menu-arrow {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n }\n /* Tetap putih saat active/hover */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item.active > .nav-link .menu-title,\n .sidebar .nav .nav-item.active > .nav-link i.menu-icon,\n .sidebar .nav .nav-item.active > .nav-link i.menu-arrow,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link .menu-title,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-icon,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-arrow {\n color: #fff !important;\n }\n\n /* ========================================================= */\n /* OVERRIDE TAMPILAN MENU AKTIF Identia */\n /* ========================================================= */\n \n /* 1. Hilangkan latar belakang biru dari seluruh blok",
"' + (d.message || 'Gagal menyimpan ke server');\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n }\n }).catch(e => {\n statusEl.innerHTML = '",
"';\n return;\n }\n\n this.dropdown.innerHTML = '';\n filtered.forEach(item => {\n const div = document.createElement('div');\n div.className = 'ss-item';\n div.textContent = item.name;\n div.addEventListener('click', () => {\n this.input.value = item.name;\n this.hidden.value = (this.useIdAsValue && item.id) ? item.id : item.name;\n this.selectedId = item.id;\n this.dropdown.classList.remove('show');\n if (this.onSelect) this.onSelect(item);\n });\n this.dropdown.appendChild(div);\n });\n }\n}\n\n// ── Init Searchable Selects ──\nlet ssProvinsi, ssKabupaten, ssKecamatan, ssDesa, ssKodePos;\nlet provDataLoaded = false;\n\nfunction openEditAddressModal() {\n $('#modalEditAddress').modal('show');\n initAddressSelects();\n}\n\nfunction initAddressSelects() {\n if (!ssProvinsi) {\n ssProvinsi = new SearchableSelect('ssProvinsi', 'hid_provinsi', {\n onSelect: (item) => { loadAddrKabupaten(item.id); }\n });\n ssKabupaten = new SearchableSelect('ssKabupaten', 'hid_kabupaten', {\n onSelect: (item) => { loadAddrKecamatan(item.id); }\n });\n ssKecamatan = new SearchableSelect('ssKecamatan', 'hid_kecamatan', {\n onSelect: (item) => { loadAddrDesa(item.id); }\n });\n ssDesa = new SearchableSelect('ssDesa', 'hid_desa', {\n onSelect: (item) => { loadAddrKodePos(item.name); }\n });\n ssKodePos = new SearchableSelect('ssKodePos', 'hid_kode_pos', {\n useIdAsValue: true\n });\n }\n loadAddrProvinsi();\n}\n\nfunction loadAddrProvinsi() {\n if (provDataLoaded) return;\n provDataLoaded = true;\n ssProvinsi.showLoading();\n fetch(API_BASE + '/provinces.json').then(r => r.json()).then(data => {\n ssProvinsi.setItems(data.map(p => ({id: p.id, name: p.name})));\n ssProvinsi.dropdown.classList.remove('show');\n // Pre-populate if saved\n if (saved.provinsi) {\n const found = data.find(p => p.name === saved.provinsi);\n if (found) {\n ssProvinsi.setValue(found.name, found.id);\n loadAddrKabupaten(found.id);\n }\n }\n }).catch(() => { ssProvinsi.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKabupaten(provId) {\n ssKabupaten.clear(); ssKecamatan.clear(); ssDesa.clear(); ssKodePos.clear();\n ssKabupaten.disable(); ssKecamatan.disable(); ssDesa.disable(); ssKodePos.disable();\n ssKabupaten.enable();\n ssKabupaten.showLoading();\n fetch(API_BASE + '/regencies/' + provId + '.json').then(r => r.json()).then(data => {\n ssKabupaten.setItems(data.map(k => ({id: k.id, name: k.name})));\n ssKabupaten.dropdown.classList.remove('show');\n if (saved.kabupaten) {\n const found = data.find(k => k.name === saved.kabupaten);\n if (found) {\n ssKabupaten.setValue(found.name, found.id);\n loadAddrKecamatan(found.id);\n }\n }\n }).catch(() => { ssKabupaten.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKecamatan(kabId) {\n ssKecamatan.clear(); ssDesa.clear(); ssKodePos.clear();\n ssKecamatan.disable(); ssDesa.disable(); ssKodePos.disable();\n ssKecamatan.enable();\n ssKecamatan.showLoading();\n fetch(API_BASE + '/districts/' + kabId + '.json').then(r => r.json()).then(data => {\n ssKecamatan.setItems(data.map(k => ({id: k.id, name: k.name})));\n ssKecamatan.dropdown.classList.remove('show');\n if (saved.kecamatan) {\n const found = data.find(k => k.name === saved.kecamatan);\n if (found) {\n ssKecamatan.setValue(found.name, found.id);\n loadAddrDesa(found.id);\n }\n }\n }).catch(() => { ssKecamatan.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrDesa(kecId) {\n ssDesa.clear(); ssKodePos.clear();\n ssDesa.disable(); ssKodePos.disable();\n ssDesa.enable();\n ssDesa.showLoading();\n fetch(API_BASE + '/villages/' + kecId + '.json').then(r => r.json()).then(data => {\n ssDesa.setItems(data.map(d => ({id: d.id, name: d.name})));\n ssDesa.dropdown.classList.remove('show');\n if (saved.desa) {\n const found = data.find(d => d.name === saved.desa);\n if (found) {\n ssDesa.setValue(found.name, found.id);\n loadAddrKodePos(found.name);\n }\n }\n }).catch(() => { ssDesa.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKodePos(desaName) {\n ssKodePos.clear();\n ssKodePos.disable();\n ssKodePos.enable();\n ssKodePos.showLoading();\n const q = encodeURIComponent(desaName);\n fetch(KODEPOS_API + '/search/?q=' + q).then(r => r.json()).then(res => {\n const results = res.data || res || [];\n const items = [];\n const seen = new Set();\n results.forEach(r => {\n const kp = r.postalcode || r.kodepos || r.postal_code || '';\n if (kp && !seen.has(kp)) {\n seen.add(kp);\n const label = kp + ' — ' + (r.village || r.urban || r.kelurahan || r.desa || desaName);\n items.push({id: kp, name: label});\n }\n });\n ssKodePos.setItems(items);\n ssKodePos.dropdown.classList.remove('show');\n if (saved.kode_pos) {\n const found = items.find(i => i.id === saved.kode_pos);\n if (found) ssKodePos.setValue(found.name, found.id);\n }\n // If no results from API, allow manual input\n if (items.length === 0) {\n ssKodePos.input.placeholder = 'Ketik kode pos manual...';\n ssKodePos.input.addEventListener('blur', function() {\n if (this.value.trim()) ssKodePos.hidden.value = this.value.trim();\n }, {once: true});\n }\n }).catch(() => {\n ssKodePos.dropdown.classList.remove('show');\n ssKodePos.input.placeholder = 'Ketik kode pos manual...';\n ssKodePos.input.addEventListener('blur', function() {\n if (this.value.trim()) ssKodePos.hidden.value = this.value.trim();\n }, {once: true});\n });\n}\n\n// ── Validasi ──\ndocument.getElementById('edit_email').addEventListener('blur', function() {\n const fb = document.getElementById('emailFeedback');\n const v = this.value.trim();\n if (!v) { fb.className = 'pf-fb'; return; }\n fb.innerHTML = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? '",
"\";\n }\n $keyboard = ['keyboard' => [[['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true]]], 'resize_keyboard' => true, 'is_persistent' => true];\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($keyboard)\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n }\n }\n}\n\n\n// Handle Pesan Diedit (Occupant mengedit pesan di Telegram)\nif (isset($update[\"edited_message\"])) {\n $chatId = $update[\"edited_message\"][\"chat\"][\"id\"];\n \n if (isset($update[\"edited_message\"][\"text\"])) {\n $editedText = $update[\"edited_message\"][\"text\"];\n $messageId = $update[\"edited_message\"][\"message_id\"];\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n // Kita tandai sebagai (Diedit) di akhir teks agar admin tahu\n $newText = htmlspecialchars($editedText) . \"",
"Opsi Perangkat Lain",
"Kembali ke beranda",
"QR Code Registrasi Foto",
"function filterNtf(event, filterType, el) {\n event.preventDefault();\n \n // Update active class on filter tags\n document.querySelectorAll('.ntf-filter-item').forEach(e => e.classList.remove('active'));\n el.classList.add('active');\n \n // Logic to show/hide based on filterType\n const activeWrap = document.getElementById('activeListWrapper');\n const trashWrap = document.getElementById('trashListWrapper');\n const mainTitle = document.getElementById('mainListTitle');\n const btnEmpty = document.getElementById('btnEmptyTrash');\n \n if(filterType === 'trash') {\n activeWrap.style.display = 'none';\n trashWrap.style.display = 'block';\n mainTitle.textContent = 'Notifikasi di Sampah';\n btnEmpty.style.display = 'block';\n return;\n }\n \n activeWrap.style.display = 'block';\n trashWrap.style.display = 'none';\n mainTitle.textContent = 'Daftar Notifikasi Terbaru';\n btnEmpty.style.display = 'none';\n\n const allCards = activeWrap.querySelectorAll('.ntf-card-item');\n allCards.forEach(card => {\n let isMatch = false;\n if(filterType === 'all') isMatch = true;\n if(filterType === 'unread' && card.dataset.unread === '1') isMatch = true;\n if(filterType === 'danger' && card.dataset.danger === '1') isMatch = true;\n \n card.style.display = isMatch ? 'flex' : 'none';\n });\n \n // Hide date headers if they have no visible descendants\n document.querySelectorAll('.ntf-group-wrap').forEach(wrap => {\n const visibleCards = wrap.querySelectorAll('.ntf-card-item[style*=\"display: flex\"], .ntf-card-item:not([style*=\"display: none\"])');\n \n // Find the corresponding date-hdr using class matching\n const classList = Array.from(wrap.classList).find(c => c.startsWith('ntf-date-group-'));\n const hdr = document.querySelector('.ntf-date-hdr.'+classList);\n \n if(visibleCards.length === 0) {\n wrap.style.display = 'none';\n if(hdr) hdr.style.display = 'none';\n } else {\n wrap.style.display = 'block';\n if(hdr) hdr.style.display = 'block';\n }\n });\n}\n\nfunction showNtf(el, id) {\n const isDanger = el.dataset.danger === '1';\n \n // Fill data\n document.getElementById('nmdl-title').textContent = el.dataset.title;\n document.getElementById('nmdl-msg').textContent = el.dataset.msg;\n document.getElementById('nmdl-dtl').textContent = el.dataset.detail;\n document.getElementById('nmdl-time').textContent = el.dataset.time + ' - ' + el.querySelector('.nc-ago').textContent;\n \n // Icon\n document.getElementById('nmdl-icn').className = 'mdi ' + el.dataset.icnclass;\n \n // Theme\n const hdr = document.getElementById('nmdl-hdr');\n if(isDanger) {\n hdr.parentElement.className = 'modal-content nt-modal-simple modal-theme-danger';\n } else {\n hdr.parentElement.className = 'modal-content nt-modal-simple modal-theme-info';\n }\n\n // Mark as read conceptually and in DB\n if (el.classList.contains('unread')) {\n el.classList.remove('unread');\n el.dataset.unread = '0';\n \n // Update unread count in UI\n let cnt = document.getElementById('cntUnread');\n if(cnt) {\n let n = parseInt(cnt.textContent) - 1;\n cnt.textContent = n;\n if(n",
"');\n $('#esp32-ssid').text('Unknown');\n $('#esp32-ip').text('Unknown');\n }\n }, 'json').fail(function() {\n console.error(\"Failed to poll ESP32 status\");\n });\n }\n \n // Poll every 3 seconds\n setInterval(pollESP32Status, 3000);\n pollESP32Status();\n\n // SIMULATE BATTERY FULL (By clicking on battery percentage gauge)\n $('#battery-percent-wrap').css('cursor', 'pointer').attr('title', 'Click to simulate Battery Full').on('click', function() {\n batLevel = 100;\n $.post('api_notifications.php', {\n action: 'create_notification',\n icon: 'mdi-battery-charging-100',\n title: 'Baterai Smart Door Penuh',\n message: 'Kapasitas baterai telah mencapai 100%.',\n detail: 'Pengecasan dapat dihentikan untuk menjaga umur baterai.',\n is_danger: 0\n });\n alert('Simulasi Baterai Penuh dipicu! Cek notifikasi.');\n });\n\n // ===== BATTERY MONITORING CHART =====\n var maxPts = 40, batLevel = 85, batLabels = [], batData = [];\n var now = new Date();\n for (var i = maxPts - 1; i >= 0; i--) {\n var t = new Date(now.getTime() - i * 2000);\n batLabels.push(t.toLocaleTimeString('id-ID', {hour:'2-digit', minute:'2-digit', second:'2-digit'}));\n batData.push(batLevel + Math.random() * 2 - 1);\n }\n var ctx = document.getElementById('battery-chart').getContext('2d');\n var grad = ctx.createLinearGradient(0, 0, 0, 200);\n grad.addColorStop(0, 'rgba(75,73,172,0.25)');\n grad.addColorStop(1, 'rgba(75,73,172,0.02)');\n var batChart = new Chart(ctx, {\n type:'line',\n data:{ labels:batLabels, datasets:[{ label:'Battery %', data:batData, borderColor:'#4B49AC', backgroundColor:grad, borderWidth:2.5, fill:true, tension:0.4, pointRadius:0, pointHoverRadius:5, pointHoverBackgroundColor:'#4B49AC' }] },\n options:{ responsive:true, maintainAspectRatio:false, animation:{duration:600}, plugins:{ legend:{display:false}, tooltip:{ backgroundColor:'#2c2e33', padding:10, cornerRadius:8, callbacks:{ label:function(c){ return c.parsed.y.toFixed(1)+'%'; } } } }, scales:{ x:{ display:true, grid:{display:false}, ticks:{font:{size:10},color:'#bbb',maxRotation:0,maxTicksLimit:8} }, y:{ display:true, min:0, max:100, grid:{color:'#f5f5f5'}, ticks:{font:{size:10},color:'#bbb',callback:function(v){return v+'%';}} } } }\n });\n setInterval(function() {\n batLevel -= (Math.random()*0.5+0.1);\n if(batLevel",
"Belum Dibaca",
"tag - check immediately\n if (typeof faceapi === 'undefined') {\n showLibError();\n return;\n }\n setProgress(20);\n loadText.textContent = 'Library dimuat. Memulai kamera...';\n startAll();\n\n function showLibError() {\n camLoading.style.display = 'none';\n document.getElementById('errTitle').textContent = 'Failed Memuat Library';\n document.getElementById('errMsg').textContent = 'Library deteksi wajah tidak dapat dimuat. Periksa koneksi internet Anda dan coba lagi.';\n document.getElementById('errIconWrap').className = 'err-icon invalid';\n document.getElementById('errIcon').className = 'mdi mdi-cloud-off-outline';\n var retryBtn = document.getElementById('btnRetryLib');\n if (retryBtn) retryBtn.style.display = 'inline-flex';\n showPage(pgError);\n }\n\n function startAll() {\n var camPromise = startCamera();\n setProgress(30);\n\n // Load TinyFaceDetector\n loadText.textContent = 'Memuat model deteksi wajah...';\n loadModels(0);\n\n camPromise.then(function(ok) {\n cameraStarted = ok;\n setProgress(50);\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n function loadModels(urlIdx) {\n if (urlIdx >= MODEL_URLS.length) {\n camLoading.style.display = 'none';\n setStatus('err', 'Failed memuat model deteksi wajah');\n return;\n }\n var url = MODEL_URLS[urlIdx];\n Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(url),\n faceapi.nets.faceLandmark68Net.loadFromUri(url)\n ]).then(function() {\n modelLoaded = true;\n landmarkLoaded = true;\n setProgress(80);\n loadText.textContent = 'Model siap!';\n onReadyCheck();\n }).catch(function() {\n loadModels(urlIdx + 1);\n });\n }\n\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n setProgress(100);\n setTimeout(function() {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }, 400);\n }\n }\n\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
"Sisa waktu: 5:00",
"/* CSS for Chat Interface */\n.chat-wrapper {\n display: flex;\n height: 600px;\n background: #fff;\n border-radius: 12px;\n overflow: hidden;\n}\n\n/* Custom Minimalist SweetAlert */\ndiv.swal2-popup.swal2-custom-small {\n width: 270px !important;\n padding: 10px 15px 15px 15px !important;\n border-radius: 12px !important;\n}\n.swal2-custom-small .swal2-icon {\n width: 38px !important;\n height: 38px !important;\n margin: 5px auto 5px auto !important;\n border-width: 2px !important;\n}\n.swal2-custom-small .swal2-icon .swal2-icon-content {\n font-size: 24px !important;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.swal2-custom-small .swal2-title {\n font-size: 15px !important;\n padding: 0 !important;\n margin: 0 !important;\n font-weight: 600 !important;\n}\n.swal2-custom-small .swal2-html-container {\n font-size: 12px !important;\n margin: 4px 0 10px 0 !important;\n color: #666;\n}\n.swal2-custom-small .swal2-actions {\n margin-top: 5px;\n gap: 6px;\n}\n.swal2-custom-small .swal2-actions button {\n font-size: 12px !important;\n padding: 6px 10px !important;\n border-radius: 6px !important;\n border: none !important;\n outline: none !important;\n box-shadow: none !important;\n color: #fff !important;\n}\n.swal2-custom-small .swal-btn-delete-all { background-color: #ef4444 !important; }\n.swal2-custom-small .swal-btn-delete-me { background-color: #f59e0b !important; }\n.swal2-custom-small .swal-btn-cancel { background-color: #94a3b8 !important; }\n\n.chat-sidebar {\n width: 340px;\n border-right: 1px solid #f0f0f0;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n}\n.chat-main {\n flex: 1;\n display: flex;\n flex-direction: column;\n background: #fafbfe;\n position: relative;\n}\n.chat-search-header {\n padding: 18px 20px;\n border-bottom: 1px solid #f0f0f0;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.chat-search-header img {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n object-fit: cover;\n}\n.chat-search-box {\n flex: 1;\n position: relative;\n}\n.chat-search-box input {\n width: 100%;\n padding: 8px 16px 8px 36px;\n border-radius: 20px;\n border: 1px solid #e0e0e0;\n font-size: 13px;\n outline: none;\n transition: 0.2s;\n}\n.chat-search-box input:focus {\n border-color: #5856d6;\n box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);\n}\n.chat-search-box i {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n color: #999;\n font-size: 14px;\n}\n\n.chat-list-wrapper {\n flex: 1;\n overflow-y: auto;\n}\n.chat-list-wrapper::-webkit-scrollbar { width: 6px; }\n.chat-list-wrapper::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n\n.section-title {\n padding: 16px 20px 8px;\n font-size: 14px;\n font-weight: 600;\n color: #5856d6;\n}\n\n.contact-item {\n display: flex;\n padding: 12px 20px;\n gap: 12px;\n cursor: pointer;\n transition: 0.2s;\n align-items: center;\n}\n.contact-item:hover {\n background: #f8faff;\n}\n.contact-item.active {\n background: #5856d6;\n}\n.contact-item.active .ci-name { color: #fff; }\n.contact-item.active .ci-msg { color: #e0e7ff; }\n.contact-item.active .ci-time { color: #c7d2fe; }\n\n.ci-avatar {\n width: 42px;\n height: 42px;\n border-radius: 50%;\n position: relative;\n}\n.ci-avatar img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n}\n.ci-status {\n position: absolute;\n bottom: 2px;\n right: 2px;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #10b981;\n border: 2px solid #fff;\n}\n.contact-item.active .ci-status { border-color: #5856d6; }\n\n.ci-info {\n flex: 1;\n min-width: 0;\n}\n.ci-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n margin-bottom: 4px;\n}\n.ci-name {\n font-size: 14px;\n color: #333;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ci-time {\n font-size: 11px;\n color: #999;\n flex-shrink: 0;\n}\n.ci-msg {\n font-size: 12px;\n color: #666;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ci-unread-badge {\n background: #5856d6;\n color: #fff;\n font-size: 10px;\n font-weight: bold;\n padding: 2px 6px;\n border-radius: 10px;\n margin-left: 6px;\n display: inline-block;\n}\n\n/* Right Blank View */\n.chat-blank {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #666;\n}\n.chat-blank-icon {\n width: 100px;\n height: 100px;\n border-radius: 50%;\n background: #eff6ff;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 20px;\n}\n.chat-blank-icon i { font-size: 40px; color: #5856d6; }\n\n/* Right Active View */\n.chat-active-view {\n display: none;\n flex-direction: column;\n height: 100%;\n}\n.chat-header {\n padding: 16px 24px;\n background: #fff;\n border-bottom: 1px solid #f0f0f0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.ch-left {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.ch-left img {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n object-fit: cover;\n}\n.ch-name {\n font-size: 15px;\n font-weight: 600;\n color: #333;\n margin-bottom: 2px;\n}\n.ch-status {\n font-size: 12px;\n color: #666;\n}\n.ch-right {\n display: flex;\n gap: 16px;\n color: #666;\n}\n.ch-right i {\n font-size: 18px;\n cursor: pointer;\n transition: 0.2s;\n}\n.ch-right i:hover { color: #5856d6; }\n\n.chat-history {\n flex: 1;\n padding: 24px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n.chat-history::-webkit-scrollbar { width: 6px; }\n.chat-history::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }\n\n.msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n max-width: 80%;\n}\n.msg-row img {\n width: 28px;\n height: 28px;\n border-radius: 50%;\n object-fit: cover;\n flex-shrink: 0;\n}\n.msg-content {\n display: flex;\n flex-direction: column;\n}\n.msg-bubble {\n padding: 6px 7px 4px 9px;\n border-radius: 7.5px;\n font-size: 14.2px;\n line-height: 19px;\n box-shadow: 0 1px .5px rgba(11,20,26,.13);\n position: relative;\n color: #111b21;\n word-break: break-all; /* Force long URLs/strings to break anywhere */\n white-space: pre-wrap;\n}\n.msg-bubble p {\n margin-bottom: 0;\n margin-top: 0;\n}\n\n.msg-time {\n font-size: 11px;\n color: #667781;\n display: inline-flex;\n align-items: center;\n float: right;\n margin-top: 4px;\n margin-bottom: -4px;\n margin-left: 12px;\n}\n\n/* WhatsApp-like message status */\n.msg-status { display: inline-flex; align-items: center; margin-left: 3px; }\n.msg-status .mdi { font-size: 15.5px; line-height: 1; margin-top: 1px;}\n.msg-status-sending .mdi { color: #8696a0; animation: spinCheck 1s linear infinite; font-size: 13px;}\n.msg-status-sent .mdi { color: #8696a0; }\n.msg-status-delivered .mdi { color: #8696a0; }\n.msg-status-read .mdi { color: #53bdeb; }\n@keyframes spinCheck { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n\n/* Optimistic message (temporary) */\n.msg-row.msg-sending { opacity: .7; }\n.msg-row.msg-sending .msg-bubble { position: relative; }\n\n/* Occupant Msg (Left) */\n.msg-left {\n align-self: flex-start;\n}\n.msg-left .msg-bubble {\n background: #ffffff;\n border-top-left-radius: 0;\n}\n.msg-left .msg-time { color: #667781; }\n\n/* Admin Msg (Right) - Updated to Primary Color */\n.msg-right {\n align-self: flex-end;\n flex-direction: row-reverse;\n}\n.msg-right .msg-bubble {\n background: #5856d6;\n color: #ffffff;\n border-top-right-radius: 0;\n}\n.msg-right .msg-time { color: #667781; }\n.msg-right .msg-status .mdi { color: #8696a0 !important; }\n.msg-right .msg-status-read .mdi { color: #53bdeb !important; }\n.edit-tag { \n float: right !important; \n margin-top: 2px !important; \n margin-left: 8px !important; \n font-size: 11.5px !important;\n}\n.msg-right .edit-tag { color: rgba(255,255,255,0.8) !important; }\n.msg-left .edit-tag { color: #8696a0 !important; }\n\n/* Chat Footer */\n.chat-footer {\n padding: 16px 24px;\n background: #fff;\n border-top: 1px solid #f0f0f0;\n display: flex;\n flex-direction: column;\n}\n.chat-footer-top {\n display: flex;\n align-items: center;\n gap: 16px;\n width: 100%;\n}\n.reply-preview-box {\n display: none;\n width: 100%;\n margin-bottom: 12px;\n padding: 10px 14px;\n background: #f8fafc;\n border-left: 4px solid #5856d6;\n border-radius: 6px;\n position: relative;\n font-size: 13px;\n}\n.rp-close {\n position: absolute;\n right: 12px;\n top: 50%;\n transform: translateY(-50%);\n cursor: pointer;\n color: #94a3b8;\n font-size: 18px;\n}\n.rp-close:hover { color: #ef4444; }\n.rp-title { font-weight: bold; color: #5856d6; margin-bottom: 2px;}\n.rp-text { color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 20px;}\n\n.msg-reply-block {\n background: rgba(0,0,0,0.05);\n border-left: 3px solid #5856d6;\n padding: 6px 10px;\n border-radius: 4px;\n margin-bottom: 6px;\n font-size: 12px;\n cursor: pointer;\n}\n.msg-reply-block .replier { font-weight: bold; color: #5856d6; }\n.msg-right .msg-reply-block { background: rgba(255,255,255,0.2); border-color: #fff; }\n.msg-right .msg-reply-block .replier { color: #fff; }\n\n.chat-footer .input-wrapper {\n flex: 1;\n background: #f8fafc;\n border-radius: 20px;\n border: 1px solid #e2e8f0;\n display: flex;\n align-items: flex-end; /* Align to bottom for textarea growth */\n padding: 8px 16px;\n transition: 0.2s;\n}\n.chat-footer .input-wrapper:focus-within {\n border-color: #5856d6;\n background: #fff;\n box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);\n}\n.chat-footer textarea {\n flex: 1;\n border: none;\n background: transparent;\n outline: none;\n padding: 8px;\n font-size: 14px;\n color: #333;\n font-family: inherit;\n resize: none;\n max-height: 120px;\n overflow-y: auto;\n box-sizing: border-box;\n /* Hide scrollbar for cleaner look */\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.chat-footer textarea::-webkit-scrollbar {\n display: none;\n}\n\n#btnEmoji {\n background: transparent;\n border: none;\n outline: none;\n box-shadow: none;\n color: #64748b;\n font-size: 20px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n#btnEmoji:hover {\n color: #5856d6;\n}\n\n.btn-send {\n background: #5856d6;\n color: #fff;\n border: none;\n border-radius: 8px;\n padding: 10px 20px;\n font-weight: 600;\n font-size: 14px;\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n transition: 0.2s;\n box-shadow: none;\n}\n.btn-send:hover { background: #4b49ac; transform: translateY(-1px); }\n\n/* Emoji Picker */\n.emoji-picker {\n display: none; position: absolute; bottom: 70px; right: 24px;\n width: 320px; background: #fff; border-radius: 12px;\n box-shadow: 0 8px 24px rgba(0,0,0,.12); border: 1px solid #edf2f7;\n z-index: 100; overflow: hidden;\n}\n.emoji-picker.show { display: block; }\n.emoji-tabs { display: flex; border-bottom: 1px solid #f1f5f9; padding: 0 4px; }\n.emoji-tab {\n flex: 1; padding: 8px 0; text-align: center; cursor: pointer;\n font-size: 16px; opacity: .5; transition: .12s; border-bottom: 2px solid transparent;\n}\n.emoji-tab:hover { opacity: .8; }\n.emoji-tab.active { opacity: 1; border-bottom-color: #5856d6; }\n.emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; padding: 8px; max-height: 200px; overflow-y: auto; }\n.emoji-grid::-webkit-scrollbar { width: 4px; }\n.emoji-grid::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n.emoji-item { font-size: 20px; text-align: center; padding: 6px; cursor: pointer; border-radius: 6px; transition: .1s; }\n.emoji-item:hover { background: #f1f5f9; transform: scale(1.2); }\n\n\n/* Custom Context Menu */\n.chat-context-menu {\n position: fixed; z-index: 9999; width: 180px; background: #fff;\n border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.15);\n padding: 6px 0; display: none; font-size: 13px;\n border: 1px solid #edf2f7;\n}\n.chat-context-menu ul { list-style: none; padding: 0; margin: 0; }\n.chat-context-menu li {\n display: flex; align-items: center; gap: 10px; padding: 9px 16px;\n cursor: pointer; transition: background .12s; color: #334155;\n}\n.chat-context-menu li:hover { background: #f1f5f9; }\n.chat-context-menu li i { font-size: 16px; width: 18px; text-align: center; color: #64748b; }\n.chat-context-menu li.ctx-danger { color: #ef4444; }\n.chat-context-menu li.ctx-danger i { color: #ef4444; }\n.chat-context-menu .ctx-sep { height: 1px; background: #f1f5f9; margin: 4px 0; }\n\n/* Selection Mode */\n.msg-checkbox {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n font-size: 22px;\n color: #94a3b8;\n display: none;\n cursor: pointer;\n z-index: 10;\n}\n.msg-row { position: relative; transition: 0.2s; }\n.msg-row.selectable {\n cursor: pointer;\n}\n/* Admin Chat Checkbox */\n.msg-right .msg-checkbox {\n left: 15px; /* sedikit ditambah jaraknya */\n}\n.msg-right.selectable {\n padding-left: 45px; /* memberi ruang */\n}\n/* Occupant Chat Checkbox */\n.msg-left .msg-checkbox {\n position: static;\n transform: none;\n margin-left: 15px;\n align-self: center;\n order: 3;\n}\n.msg-left.selectable {\n padding-left: 0;\n}\n.msg-row.selectable .msg-checkbox {\n display: block;\n}\n.msg-row.selected .msg-checkbox i::before {\n content: \"\\F0134\"; /* mdi-checkbox-marked-circle */\n color: #5856d6;\n}\n.msg-row.selected .msg-bubble {\n background: rgba(88, 86, 214, 0.15) !important;\n color: #111b21 !important;\n}\n.msg-row.msg-right.selected .msg-bubble {\n background: rgba(88, 86, 214, 0.8) !important;\n color: #fff !important;\n}\n.ch-right-actions {\n display: none;\n align-items: center;\n gap: 15px;\n}\n.ch-right-actions i {\n font-size: 22px;\n cursor: pointer;\n color: #64748b;\n}\n.ch-right-actions i.action-del { color: #ef4444; }\n.ch-right-actions i.action-close:hover { color: #333; }\n\n/* ===== RESPONSIVE MOBILE (WhatsApp-like) ===== */\n.chat-back-btn {\n display: none;\n background: none;\n border: none;\n font-size: 22px;\n color: #5856d6;\n cursor: pointer;\n padding: 0;\n margin-right: 8px;\n line-height: 1;\n}\n@media (max-width: 768px) {\n .chat-wrapper {\n height: calc(100vh - 120px);\n }\n .chat-sidebar {\n width: 100%;\n border-right: none;\n }\n .chat-main {\n display: none !important;\n width: 100%;\n }\n .chat-wrapper.mobile-chat-open .chat-sidebar {\n display: none !important;\n }\n .chat-wrapper.mobile-chat-open .chat-main {\n display: flex !important;\n width: 100%;\n }\n .chat-back-btn {\n display: inline-flex;\n }\n .emoji-picker {\n width: 280px;\n right: 10px;\n bottom: 60px;\n }\n}",
"PIN digunakan untuk melihat password asli. Pastikan Anda",
"Hanya 1 wajah yang boleh terdeteksi",
"\"\n data-title=\"",
"Simpan kode ini.",
"Informasi pribadi dan kontak Anda,",
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-block');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_account\").value = \"BULK\"; \n document.getElementById(\"delete_account_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n });\n\n $(\"#formEditAccount\").submit(function(e) { \n e.preventDefault(); \n \n // Check face if a new photo was uploaded\n var editFoto = document.getElementById('edit_foto');\n if (editFoto.files.length > 0 && document.getElementById('faceVerifiedFlagEdit').value !== '1') {\n showCustomAlert('Foto yang diupload tidak memiliki wajah terdeteksi. Gunakan foto dengan wajah yang jelas.', 'warning');\n return;\n }\n \n var pw1 = document.getElementById(\"edit_password\").value;\n var pw2 = document.getElementById(\"edit_confirm_password\").value;\n \n if (pw1 !== \"\") {\n const pwValid = (pw1.length >= 8 && /[a-z]/.test(pw1) && /[A-Z]/.test(pw1) && /\\d/.test(pw1) && /[^A-Za-z0-9]/.test(pw1));\n if (!pwValid) {\n showCustomAlert(\"Password baru belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"edit_password\").focus();\n return;\n }\n }\n \n if (pw1 !== pw2) {\n document.getElementById(\"editPasswordMismatch\").style.display = \"block\";\n return;\n } else {\n document.getElementById(\"editPasswordMismatch\").style.display = \"none\";\n }\n \n var btn = document.getElementById(\"btnEditAccount\"); \n btn.disabled = true; \n btn.innerHTML = '",
"Password salah atau sinyal lemah.",
"Belum diisi",
"Tidak ada kegiatan di tanggal ini",
"Klik nama di sebelah kiri untuk melihat pesan otomatis dari Telegram",
"Data Account",
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalEdit\").modal(\"hide\"); \n showCustomAlert(\"Akun berhasil diubah.\");\n setTimeout(function(){ location.reload(); }, 1000);\n } else { \n showCustomAlert(d.message || \"Gagal menyimpan perubahan.\"); \n }\n }).catch(function(){ \n btn.disabled=false; \n btn.innerHTML='",
"Kelola akses penghuni dan pintu IoT,",
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n\n\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.btn-custom-edit { background: #e2e6ea; border: none; color: #212529; transition: background 0.2s ease; }\n.btn-custom-edit:hover, .btn-custom-edit:focus { background: #cbd3da; color: #212529; }",
"PIN diperlukan untuk membuka pintu secara emergency",
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalTambah\").modal(\"hide\"); \n // Langsung tampilkan modal fingerprint dan proses akun id nya\n showFingerprintModal(d.id_account);\n } else { \n showCustomAlert(d.message || \"Gagal menyimpan akun.\"); \n }\n }).catch(function(){ \n btn.disabled=false; \n btn.innerHTML='",
"\"\n data-msg=\"",
"Koneksi gagal",
"';\n });\n \n document.getElementById('btnBack').addEventListener('click', function() {\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step1-data').style.display = 'block';\n infoAlertText.innerHTML = originalInfoText;\n });",
"Password Baru tidak cocok!",
"' + msg;\n toast.style.display = 'flex';\n }\n\n /* ── Step 1: Validate Token ──── */\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n } else {\n document.getElementById('errTitle').textContent =\n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Token Sudah Digunakan' : 'Token Tidak Valid';\n document.getElementById('errMsg').textContent = d.message || 'Token tidak valid.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Gagal';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n /* ── Step 2: Load models + camera ─ */\n var modelLoaded = false;\n var cameraStarted = false;\n\n function initCamera() {\n // Wait for face-api.js library to be available, then start everything\n function waitAndInit() {\n if (typeof faceapi === 'undefined') {\n // face-api.js not loaded yet, wait\n setTimeout(waitAndInit, 200);\n return;\n }\n\n // Start camera immediately while models load in parallel\n var cameraReady = startCamera();\n\n // Load face detection model\n faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL).then(function() {\n modelLoaded = true;\n onReadyCheck();\n }).catch(function(e) {\n console.error('Model load error:', e);\n // Try alternate model URL as fallback\n faceapi.nets.tinyFaceDetector.loadFromUri('https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights').then(function() {\n modelLoaded = true;\n onReadyCheck();\n }).catch(function(e2) {\n console.error('Fallback model load error:', e2);\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat model deteksi wajah');\n });\n });\n\n cameraReady.then(function(ok) {\n cameraStarted = ok;\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n // Called when either camera or model finishes - starts detection when both ready\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n setTimeout(function() {\n if (instrPanel) instrPanel.style.opacity = '0';\n setTimeout(function() { if (instrPanel) instrPanel.style.display = 'none'; }, 500);\n }, 6000);\n }\n }\n\n // Set a timeout - if everything takes too long, show error\n setTimeout(function() {\n if (!modelLoaded || !cameraStarted) {\n if (typeof faceapi === 'undefined') {\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat library deteksi wajah');\n } else if (!cameraStarted) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak dapat dimulai');\n }\n }\n }, 20000);\n\n waitAndInit();\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
"— ESP32 hanya mendukung WPA dan WPA2. Jika router menggunakan WPA3 atau WPA2/WPA3 mixed mode, ubah ke WPA2-Only di pengaturan router.',\n '",
"';\n };\n xhr.send(fd);\n }\n\n /* ── Events ──────────────────── */\n if (btnShutter) btnShutter.addEventListener('click', capture);\n if (btnRetake) btnRetake.addEventListener('click', retake);\n if (btnSave) btnSave.addEventListener('click', save);\n\n /* ── Init ────────────────────── */\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', validateToken);\n } else {\n validateToken();\n }\n\n /* ── PWA Service Worker ──────── */\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('sw_foto.js').catch(function() {});\n }\n\n /* ── Smooth instruction fade ── */\n var ip = document.getElementById('instrPanel');\n if (ip) ip.style.transition = 'opacity 0.5s ease';\n})();",
"`;\n case 'sidebar':\n case 'header':\n case 'card':\n return '';// containers render children\n default:return '';\n }\n }\n\n // ===== DRAG (MOVE) =====\n startDrag(e,comp){\n e.preventDefault();\n const startX=e.clientX,startY=e.clientY;\n const origX=comp.x,origY=comp.y;\n const move=ev=>{\n comp.x=Math.max(0,Math.round((origX+(ev.clientX-startX))/5)*5);\n comp.y=Math.max(0,Math.round((origY+(ev.clientY-startY))/5)*5);\n const el=document.querySelector(`.tft-comp[data-id=\"${comp.id}\"]`);\n if(el){el.style.left=comp.x+'px';el.style.top=comp.y+'px';}\n if(this.selectedId===comp.id)this.updatePropXY(comp);\n };\n const up=()=>{\n document.removeEventListener('mousemove',move);\n document.removeEventListener('mouseup',up);\n this.pushHistory();\n };\n document.addEventListener('mousemove',move);\n document.addEventListener('mouseup',up);\n }\n\n // ===== RESIZE =====\n startResize(e,comp){\n e.preventDefault();e.stopPropagation();\n const startX=e.clientX,startY=e.clientY;\n const origW=comp.w,origH=comp.h;\n const move=ev=>{\n comp.w=Math.max(20,Math.round((origW+(ev.clientX-startX))/5)*5);\n comp.h=Math.max(10,Math.round((origH+(ev.clientY-startY))/5)*5);\n const el=document.querySelector(`.tft-comp[data-id=\"${comp.id}\"]`);\n if(el){el.style.width=comp.w+'px';el.style.height=comp.h+'px';el.innerHTML=this.renderCompInner(comp);\n const lbl=document.createElement('div');lbl.className='tft-comp-label';lbl.textContent=comp.type;el.appendChild(lbl);\n const rh=document.createElement('div');rh.className='resize-handle';el.appendChild(rh);}\n if(this.selectedId===comp.id)this.updatePropWH(comp);\n };\n const up=()=>{\n document.removeEventListener('mousemove',move);\n document.removeEventListener('mouseup',up);\n this.pushHistory();\n this.renderCanvas();\n };\n document.addEventListener('mousemove',move);\n document.addEventListener('mouseup',up);\n }\n\n updatePropXY(c){const ex=document.getElementById('pi_x'),ey=document.getElementById('pi_y');if(ex)ex.value=c.x;if(ey)ey.value=c.y;}\n updatePropWH(c){const ew=document.getElementById('pi_w'),eh=document.getElementById('pi_h');if(ew)ew.value=c.w;if(eh)eh.value=c.h;}\n\n // ===== PROPERTY INSPECTOR =====\n renderProps(){\n const c=this.findComp(this.currentComps(),this.selectedId);\n if(!c){this.clearProps();return;}\n const isText=['button','label','datavalue','icon'].includes(c.type);\n const isContainer=CONTAINERS.includes(c.type);\n const rgb565=hexToRGB565(c.bg||'#1e2223');\n\n let h=`",
"Jaringan ditemukan",
"Memindai jaringan WiFi di sekitar ESP32",
"Ulangi Password Baru",
"Coba ganti channel WiFi router",
"// Toggle sidebar on mobile\n$('.btn-email-sidebar-toggler').on('click', function(e){\n e.preventDefault();\n $('.email-sidebar').toggleClass('show');\n});\n\n// Select All Logic\n$('#checkAll').on('change', function() {\n $('.chk-item').prop('checked', $(this).prop('checked'));\n});\n\n// Single check stop propagation\n$('.email-checkbox label, .email-checkbox input').on('click', function(e){\n // Don't trigger the email open\n e.stopPropagation();\n});\n\n// Star toggle logic\n$('.email-star').on('click', function(e) {\n e.stopPropagation();\n let id = $(this).data('id');\n let icon = $(this);\n \n $.post('history_ajax.php', { action: 'toggle_star', id: id }, function(res) {\n if(res.status === 'success') {\n if(res.starred == 1) {\n icon.removeClass('mdi-star-outline text-muted').addClass('mdi-star text-warning');\n } else {\n icon.removeClass('mdi-star text-warning').addClass('mdi-star-outline text-muted');\n }\n }\n });\n});\n\n// Open Email Detail View\nlet currentOpenId = null;\n\nfunction openDetail(data, element) {\n currentOpenId = data.id;\n \n // Update UI elements\n $('#detSubject, .detail-subject').text(data.activity);\n $('#detName').text(data.name);\n $('#detNik').text('NIK/NIP: ' + data.nik);\n $('#detTime').text(data.time_full);\n $('#detLargeFoto').attr('src', data.foto);\n $('#detAvatar').attr('src', data.occupant_foto);\n $('#detActivityText').text(data.activity);\n $('#detSign').text(data.name);\n $('#detHp').text('Tel: ' + data.hp);\n \n if (data.is_starred == 1) {\n $('#detStarIcon').removeClass('mdi-star-outline').addClass('mdi-star text-warning');\n } else {\n $('#detStarIcon').removeClass('mdi-star text-warning').addClass('mdi-star-outline');\n }\n \n // Switch views\n $('#emailListWrapper, .email-toolbar').addClass('d-none');\n $('#emailDetailWrapper').removeClass('d-none');\n \n // Remove bold if it was unread\n $(element).parent().removeClass('font-weight-bold').addClass('read');\n \n // Remove the green dot visually\n $(element).find('.unread-dot').fadeOut(200, function(){ $(this).remove(); });\n \n // Mark as read in DB via AJAX if not already\n if (data.is_read == 0) {\n $.post('history_ajax.php', { action: 'mark_read', id: data.id });\n data.is_read = 1; // Update local data\n }\n}\n\n// Back to Inbox\n$('#btnBackToInbox').on('click', function() {\n $('#emailDetailWrapper').addClass('d-none');\n $('#emailListWrapper, .email-toolbar').removeClass('d-none');\n});\n\n// Detail Delete Button\n$('#btnDetailDelete').on('click', function() {\n if(currentOpenId) {\n $.post('history_ajax.php', { action: 'delete', id: currentOpenId }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n }\n});\n\n// Inline Hover Delete button\n$('.btn-del').on('click', function(e){\n e.stopPropagation();\n let id = $(this).data('id');\n $.post('history_ajax.php', { action: 'delete', id: id }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n});\n\n// Inline Hover Restore button\n$('.btn-res').on('click', function(e){\n e.stopPropagation();\n let id = $(this).data('id');\n $.post('history_ajax.php', { action: 'restore', id: id }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n});\n\n// Bulk Delete\n$('#btnDeleteAll').on('click', function() {\n let ids = [];\n $('.chk-item:checked').each(function(){ ids.push($(this).val()); });\n if(ids.length > 0) {\n $.post('history_ajax.php', { action: 'delete', ids: ids }, function(res){\n if(res.status === 'success') window.location.reload();\n });\n }\n});\n\n// Bulk Restore\n$('#btnRestoreAll').on('click', function() {\n let ids = [];\n $('.chk-item:checked').each(function(){ ids.push($(this).val()); });\n if(ids.length > 0) {\n $.post('history_ajax.php', { action: 'restore', ids: ids }, function(res){\n if(res.status === 'success') window.location.reload();\n });\n }\n});\n\n// Hard Delete All\nlet deleteIds = [];\n$('#btnDeleteAllHard').on('click', function() {\n deleteIds = [];\n $('.chk-item:checked').each(function(){ deleteIds.push($(this).val()); });\n if(deleteIds.length > 0) {\n $('#modalDeleteHistory').modal('show');\n }\n});\n\n$('#formDeleteHistory').on('submit', function(e) {\n e.preventDefault();\n if(deleteIds.length > 0) {\n $('#btnDeleteHistoryConfirm').prop('disabled', true).html('",
"ke Admin.\\n\\nGambar, stiker, dokumen, atau pesan suara akan otomatis dihapus oleh sistem.\";\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML'\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n exit;\n }\n \n // Handle Teks Biasa (Occupant membalas/chat ke Admin atau kirim command khusus)\n if (isset($update[\"message\"][\"text\"])) {\n $textMessage = trim($update[\"message\"][\"text\"]);\n \n // Verifikasi bahwa user telegram ini ada telegram_id nya di sistem\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $id_occupant = $occupant['id_occupant'];\n \n // Dapatkan URL dasar untuk Web App\n $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? \"https\" : \"http\";\n $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';\n $uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\\\\');\n $baseUrl = $protocol . \"://\" . $host . $uri;\n \n // --- Cek Command Khusus (Tidak masuk ke database chats admin) ---\n if ($textMessage === '/help') {\n $text = \"📚",
"Full Name wajib diisi.",
"Penting!",
"Ketik kabupaten/kota lalu pilih...",
"Simpan';\n if (d.status === \"success\") { $(\"#modalTambah\").modal(\"hide\"); document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"hasilCodeRegister\").value = d.code_register; $(\"#modalCodeRegister\").modal({ backdrop:'static', keyboard:false }); if (d.id_occupant) { generateQrInCodeModal(d.id_occupant); } }\n else { showCustomAlert(d.message || \"Gagal menyimpan data.\"); }\n }).catch(function(){ btn.disabled=false; btn.innerHTML='",
"DATA & TYPOGRAPHY",
"Masukkan PIN 6 digit untuk melihat password asli.",
"Pastikan router 2.4GHz aktif"
]