TKK_E32230273/dashboard.php

1649 lines
78 KiB
PHP

<?php include 'header.php'; ?>
<?php
if (session_status() === PHP_SESSION_NONE)
session_start();
include 'koneksi.php';
// Ambil jumlah occupant dengan status approved
$stmt = $pdo->query("SELECT count(*) as total_occupant FROM occupant WHERE status = 'approved'");
$occupant = $stmt->fetch();
$total_occupant = $occupant['total_occupant'];
// Ambil jumlah account
$stmt = $pdo->query("SELECT count(*) as total_account FROM account");
$account = $stmt->fetch();
$total_account = $account['total_account'];
// Ambil data profile user yang login
$dashUser = null;
try {
$stmtUser = $pdo->prepare("SELECT a.name, a.username FROM account a WHERE a.id_account = ?");
$stmtUser->execute([isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 0]);
$dashUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
}
$dashFotoSrc = 'images/admin.png';
$dashName2 = $dashUser ? $dashUser['name'] : 'User';
$dashUsername = $dashUser ? $dashUser['username'] : 'unknown';
$dashFingerDevice = '';
$dashHasFinger = false;
// Ambil data terakhir login
$lastLog = null;
try {
// Pastikan tabel ada
$pdo->exec("CREATE TABLE IF NOT EXISTS login_history (
id_log INT AUTO_INCREMENT PRIMARY KEY,
id_account INT NOT NULL,
device_info VARCHAR(255),
ip_address VARCHAR(50),
login_time DATETIME DEFAULT CURRENT_TIMESTAMP
)");
$stmtLastLog = $pdo->prepare("SELECT * FROM login_history WHERE id_account = ? ORDER BY login_time DESC LIMIT 1 OFFSET 1");
$stmtLastLog->execute([isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 0]);
$lastLog = $stmtLastLog->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
}
?>
<!-- partial -->
<div class="main-panel">
<div class="content-wrapper">
<div class="row">
<div class="col-md-12 mb-2">
<div class="row">
<div class="col-12 col-xl-8 mb-2 mb-xl-0">
<?php
if (session_status() === PHP_SESSION_NONE)
session_start();
$dashName = isset($_SESSION['name']) ? $_SESSION['name'] : 'User';
?>
<h3 class="font-weight-bold">
<span id="typed-text"></span><span class="cursor-type" id="cursor1"></span>
</h3>
<h6 class="font-weight-normal mb-0"
style="max-width: 750px; line-height: 1.6; font-family: 'Inter', sans-serif;">
<span id="typed-sub"></span><span class="cursor-type" id="cursor2" style="display: none;"></span>
</h6>
</div>
<style>
.cursor-type {
display: inline-block;
width: 5px;
height: 1.05em;
background: linear-gradient(to bottom,
rgba(71, 71, 161, 0.45) 0%,
rgba(71, 71, 161, 0.45) 55%,
rgba(0, 128, 0, 0.45) 70%,
rgba(255, 193, 0, 0.45) 85%,
rgba(255, 0, 0, 0.45) 100%);
vertical-align: middle;
animation: cursorBlink 1.2s step-end infinite;
margin-left: 6px;
margin-top: -4px;
border-radius: 5px;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.8)) blur(2.5px);
box-shadow: 0 0 5px 2px rgba(255, 255, 255, 0.7), 0 0 10px 4px rgba(255, 255, 255, 0.4);
}
@keyframes cursorBlink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const userName = "<?= htmlspecialchars(addslashes($dashName)) ?>";
const text1 = "Welcome, " + userName + "! Monitor and manage your IoT Smart Door system seamlessly.";
const text2 = "Manage your system completely from one place: Organize Web Accounts, configure Room Occupants access, monitor real-time Access History, receive instant Notifications, and communicate across users via Live Chats.";
const el1 = document.getElementById('typed-text');
const el2 = document.getElementById('typed-sub');
const cur1 = document.getElementById('cursor1');
const cur2 = document.getElementById('cursor2');
let idx1 = 0;
let idx2 = 0;
function typeText1() {
if (idx1 < text1.length) {
el1.innerHTML += text1.charAt(idx1);
idx1++;
setTimeout(typeText1, Math.random() * 50 + 20);
} else {
cur1.style.display = 'none';
cur2.style.display = 'inline-block';
setTimeout(typeText2, 400); // delay before starting subtitle
}
}
function typeText2() {
if (idx2 < text2.length) {
el2.innerHTML += text2.charAt(idx2);
idx2++;
setTimeout(typeText2, Math.random() * 30 + 10); // faster typing for subtitle
} else {
el2.innerHTML = "Manage your system completely from one place: Organize <b>Web Accounts</b>, configure <b>Room Occupants</b> access, monitor real-time <b>Access History</b>, receive instant <b>Notifications</b>, and communicate across users via <b>Live Chats</b>.";
}
}
if (document.referrer && (document.referrer.includes('login') || document.referrer.includes('index'))) {
sessionStorage.removeItem('dashboard_typed');
}
const typedFlag = sessionStorage.getItem('dashboard_typed');
if (typedFlag) {
// Already typed, just show the final text
el1.innerHTML = text1;
el2.innerHTML = "Manage your system completely from one place: Organize <b>Web Accounts</b>, configure <b>Room Occupants</b> access, monitor real-time <b>Access History</b>, receive instant <b>Notifications</b>, and communicate across users via <b>Live Chats</b>.";
cur1.style.display = 'none';
cur2.style.display = 'inline-block';
} else {
// Run typing animation
sessionStorage.setItem('dashboard_typed', 'true');
setTimeout(typeText1, 600);
}
});
</script>
<div class="col-12 col-xl-4">
<div class="justify-content-end d-flex">
<style>
@property --bg-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.animated-clock-container {
border-radius: 50px;
border: 1px solid transparent;
background-image:
linear-gradient(#fff, #fff),
conic-gradient(from var(--bg-angle),
#dcf2fd 0%,
#2b8afb 15%,
#8b24f5 40%,
#df12c0 60%,
#8b24f5 75%,
#2b8afb 90%,
#dcf2fd 100%);
background-origin: border-box;
background-clip: padding-box, border-box;
box-sizing: border-box;
animation: spin-gradient 3s linear infinite;
}
@keyframes spin-gradient {
to {
--bg-angle: 360deg;
}
}
</style>
<div class="d-flex align-items-center animated-clock-container" style="padding:6px 16px;">
<span id="dashLiveClock" style="font-size:13px;font-weight:600;color:#2c2e33;"></span>
<span class="mx-2" style="color:#ccc;">|</span>
<span id="dashLiveDate" style="font-size:12px;color:#64748b;"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<?php
$lastLogDevice = 'Unknown Device';
$lastLogTime = '-';
$lastLogDesc = 'No history yet';
if ($lastLog) {
$uaLog = strtolower($lastLog['device_info']);
if (strpos($uaLog, 'windows') !== false)
$lastLogDevice = 'Windows PC';
elseif (strpos($uaLog, 'mac') !== false)
$lastLogDevice = 'Mac OS PC';
elseif (strpos($uaLog, 'android') !== false)
$lastLogDevice = 'Android Device';
elseif (strpos($uaLog, 'iphone') !== false || strpos($uaLog, 'ipad') !== false)
$lastLogDevice = 'iOS Device';
elseif (strpos($uaLog, 'linux') !== false)
$lastLogDevice = 'Linux PC';
$tLog = strtotime($lastLog['login_time']);
$lastLogTime = date('H:i:s', $tLog);
$lastLogDesc = date('l, d F Y', $tLog);
}
?>
<div class="row mt-3">
<div class="col-md-3 stretch-card transparent mb-4">
<div class="card" style="background:#ffc100;color:#fff;border-radius:16px;">
<div class="card-body" style="position:relative;padding-bottom:36px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="mb-0">Door Status</p>
<span id="dash-top-door-pill" class="badge"
style="background: #ffffff;color:#059669;font-size:9px;padding:3px 8px;border-radius:20px;font-weight:700;box-shadow:0 2px 4px rgba(0,0,0,0.05);"><i
class="mdi mdi-lock mr-1"></i>LOCKED</span>
</div>
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="d-flex align-items-center">
<span style="font-size:11px;">Time</span>
</div>
<span style="font-size:11px; font-weight: 500;" id="dash-top-door-time">--:--:--</span>
</div>
<div class="d-flex align-items-center justify-content-between mb-0">
<div class="d-flex align-items-center">
<span style="font-size:11px;">Date</span>
</div>
<span style="font-size:11px; font-weight: 500;" id="dash-top-door-date">-</span>
</div>
<div style="position:absolute;bottom:10px;left:20px;">
<span style="font-size:11px; font-weight:500; opacity:0.7;">Real-time
monitoring</span>
</div>
</div>
</div>
</div>
<div class="col-md-3 stretch-card transparent mb-4">
<div class="card card-dark-blue">
<div class="card-body" style="position:relative;">
<div id="esp32-status-icon-container" style="position:absolute; top:20px; right:20px;">
<!-- Signal / Loading icon will appear here -->
</div>
<p class="mb-3">ESP32 Microcontroller</p>
<div style="font-size:12px;">
<p class="mb-1"><span class="esp32-wifi-dot" id="wifi-dot"
style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#10b981;margin-right:5px;"></span>
<span id="wifi-status-text" style="color:#ffffff;">WiFi Connected</span>
</p>
<p class="mb-1">SSID: <strong id="esp32-ssid">FiberID_Network</strong></p>
<p class="mb-0">IP: <strong id="esp32-ip" style="font-family:monospace;color:#ffffff;">Unknown</strong>
</p>
</div>
</div>
</div>
</div>
<div class="col-md-3 stretch-card transparent mb-4">
<div class="card card-tale">
<div class="card-body">
<p class="mb-4">Occupant Data</p>
<p class="fs-30 mb-2" id="occupant-count"><?= $total_occupant ?></p>
<p>Total Occupants Registered</p>
</div>
</div>
</div>
<div class="col-md-3 stretch-card transparent mb-4">
<div class="card card-light-blue">
<div class="card-body">
<p class="mb-4">Account Data</p>
<p class="fs-30 mb-2" id="account-count"><?= $total_account ?></p>
<p>Total Accounts Active</p>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Login History Card (Left - 5/12) -->
<div class="col-md-5 grid-margin stretch-card">
<div class="card" style="border:1px solid #e2e8f0;border-radius:10px;width:100%;">
<div class="card-body" style="padding:1rem;">
<p class="card-title mb-0" style="font-size:0.95rem;">Login History</p>
<small class="text-muted">Your recent login activity</small>
<div id="loginHistoryCardBody" class="mt-3" style="max-height:260px;overflow-y:auto;">
<div class="text-center py-3">
<i class="mdi mdi-loading mdi-spin" style="font-size:28px;color:#4B49AC;"></i>
<p class="mt-2 mb-0" style="font-size:11px;color:#999;">Loading login history...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Change WiFi Card (Center - 4/12) -->
<div class="col-md-4 grid-margin stretch-card">
<div class="card" style="border:1px solid #e2e8f0;border-radius:10px;width:100%;">
<div class="card-body" style="padding:1rem;">
<p class="card-title mb-0" style="font-size:0.95rem;">Change WiFi
</p>
<small class="text-muted">Change WiFi configuration</small>
<div class="form-group mb-2 mt-3">
<label style="font-size:10px;font-weight:600;color:#555;margin-bottom:3px;">Target SSID</label>
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="new-ssid" placeholder="Use the right button ->" required
readonly
style="font-size:11px;border-radius:6px 0 0 6px;border:1px solid #e2e8f0;background-color:#f8f9fa;cursor:not-allowed;">
<div class="input-group-append">
<button class="btn btn-primary" type="button" id="btnScanWifi"
style="border-radius:0 6px 6px 0;border-color:#4b49ac;font-size:11px;" title="Search WiFi"><i
class="mdi mdi-magnify"></i></button>
</div>
</div>
</div>
<div class="form-group mb-2">
<label style="font-size:10px;font-weight:600;color:#555;margin-bottom:3px;">Password</label>
<div class="input-group input-group-sm">
<input type="password" class="form-control" id="new-wifi-pass" placeholder="Enter password" required
style="font-size:11px;border-radius:6px 0 0 6px;border:1px solid #e2e8f0;">
<div class="input-group-append">
<button class="btn btn-light" type="button" id="toggleNewPass"
style="border-radius:0 6px 6px 0;border-color:#e2e8f0;font-size:11px;"><i
class="mdi mdi-eye-outline"></i></button>
</div>
</div>
</div>
<div class="mt-1 mb-2"
style="background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;display:flex;align-items:flex-start;gap:6px;padding:7px 10px;font-size:9px;color: #742e03ff;">
<i class="mdi mdi-alert" style="font-size:14px;color:#f59e0b;flex-shrink:0;margin-top:1px;"></i>
<div style="flex:1;line-height:1.3;"><strong>Change ESP32 WiFi Network:</strong> Use this menu to switch
the ESP32 to a different WiFi connection (e.g., from WiFi A to WiFi B). Press the <b>Scan</b> button
next to the SSID name to search for nearby WiFi networks.</div>
</div>
<button type="button" class="btn btn-sm btn-block" id="saveWifiBtn"
style="background:#4B49AC;color:#fff;border-radius:6px;font-weight:600;font-size:11px;padding:6px;">
<i class="mdi mdi-check mr-1"></i> Save
</button>
</div>
</div>
</div>
<!-- Profile Card (Right - 3/12) -->
<style>
.profile-animated-card::before {
content: '';
position: absolute;
width: 120px;
height: 200%;
/* Gradasi: Biru Muda -> Biru Tua -> Putih (Agak Tebal) -> Ungu Tua -> Ungu Muda -> Biru Muda */
background-image: linear-gradient(180deg, #dcf2fd, #2b8afb, #ffffff 40%, #ffffff 60%, #8b24f5, #df12c0, #dcf2fd);
animation: rotBGimgProfile 3s linear infinite;
transition: all 0.2s linear;
z-index: 0;
top: 50%;
left: 50%;
}
@keyframes rotBGimgProfile {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.profile-animated-card::after {
content: '';
position: absolute;
background: #ffffff;
inset: 1.5px;
border-radius: 8.5px;
z-index: 1;
}
</style>
<div class="col-md-3 grid-margin">
<div class="card profile-animated-card"
style="border: none; border-radius: 10px; position: relative; overflow: hidden; width:100%;">
<div class="card-body" style="z-index:2;padding:1rem;text-align:center; position:relative;">
<p class="card-title mb-0" style="font-size:0.95rem;">My Profile</p>
<small class="text-muted">Logged in account</small>
<div style="margin-top:12px;">
<div
style="width:86px;height:86px;border-radius:50%;overflow:hidden;border:3px solid #e2e8f0;margin:0 auto;">
<img src="<?= htmlspecialchars($dashFotoSrc) ?>" alt="Profile"
style="width:100%;height:100%;object-fit:cover;">
</div>
</div>
<!-- Name & Username -->
<div style="margin-top:8px;">
<p class="mb-0" style="font-size:13px;font-weight:700;color:#1a1a1a;"><?= htmlspecialchars($dashName2) ?>
</p>
<p class="mb-0" style="font-size:11px;color:#94a3b8;">@<?= htmlspecialchars($dashUsername) ?></p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
<!-- WIFI CHANGE MODAL (Removed) -->
<!-- ==================== MODAL: WIFI SCAN ==================== -->
<div class="modal fade" id="wifiScanModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content mdl-card">
<div class="modal-header">
<h6 class="modal-title font-weight-bold"><i class="mdi mdi-wifi-marker mr-1 text-primary"></i> Scan WiFi</h6>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body text-center">
<!-- Scanning Animation -->
<div id="wifiScanAnim">
<div style="margin:16px auto 20px;">
<svg width="80" height="70" viewBox="0 0 80 70" fill="none" style="display:block;margin:0 auto;">
<!-- Antenna head (circle) -->
<circle cx="40" cy="28" r="7" stroke="#4B49AC" stroke-width="3" fill="none" />
<!-- Antenna stick -->
<rect x="38" y="35" width="4" height="22" rx="2" fill="#4B49AC" />
<!-- LEFT arcs (inner to outer) -->
<path class="wifi-arc arc-l1" d="M26 28 A14 14 0 0 1 26 14" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-l2" d="M20 32 A20 20 0 0 1 20 10" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-l3" d="M14 36 A26 26 0 0 1 14 6" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-l4" d="M8 40 A32 32 0 0 1 8 2" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<!-- RIGHT arcs (inner to outer) -->
<path class="wifi-arc arc-r1" d="M54 14 A14 14 0 0 1 54 28" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-r2" d="M60 10 A20 20 0 0 1 60 32" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-r3" d="M66 6 A26 26 0 0 1 66 36" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
<path class="wifi-arc arc-r4" d="M72 2 A32 32 0 0 1 72 40" stroke="#4B49AC" stroke-width="3"
stroke-linecap="round" fill="none" />
</svg>
</div>
<p style="font-size:12px;color:#666;margin:0;">Scanning for WiFi networks near the ESP32<span
class="scan-dots">...</span></p>
</div>
<!-- Network List -->
<div id="wifiNetworkList" style="display:none;text-align:left;">
<p
style="font-size:10px;color:#999;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;">
<i class="mdi mdi-access-point-network mr-1"></i> Networks Found
</p>
<div id="wifiListItems" style="max-height:240px;overflow-y:auto;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== MODAL: SAVE PROGRESS (TRANSPARENT) ==================== -->
<div class="modal fade" id="wifiSaveModal" tabindex="-1" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog modal-sm" id="wifiSaveModalDialog">
<div class="modal-content mdl-card" style="border:none;overflow:visible;background:transparent;box-shadow:none;">
<div class="save-glass-card">
<!-- Step 1: Connecting -->
<div id="saveStep1" class="save-step text-center">
<div class="save-orbit-wrap">
<div class="save-orbit-dot" style="animation-delay:0s;"></div>
<div class="save-orbit-dot" style="animation-delay:0.4s;"></div>
<div class="save-orbit-dot" style="animation-delay:0.8s;"></div>
<i class="mdi mdi-wifi save-center-icon" style="color:#4B49AC;"></i>
</div>
<h6 class="font-weight-bold mb-1">Connecting</h6>
<p style="font-size:11px;color:#888;margin-bottom:12px;">Sending configuration to ESP32...</p>
<div class="save-bar-track">
<div class="save-bar-fill" style="background:linear-gradient(90deg,#4B49AC,#7978E9);"></div>
</div>
</div>
<!-- Step 2: Configuring -->
<div id="saveStep2" class="save-step text-center" style="display:none;">
<div class="save-orbit-wrap">
<i class="mdi mdi-cog save-center-icon" style="color:#f59e0b;animation:spinSave 2s linear infinite;"></i>
<div class="save-particle" style="--angle:0deg;--delay:0s;"></div>
<div class="save-particle" style="--angle:60deg;--delay:0.2s;"></div>
<div class="save-particle" style="--angle:120deg;--delay:0.4s;"></div>
<div class="save-particle" style="--angle:180deg;--delay:0.6s;"></div>
<div class="save-particle" style="--angle:240deg;--delay:0.8s;"></div>
<div class="save-particle" style="--angle:300deg;--delay:1s;"></div>
</div>
<h6 class="font-weight-bold mb-1">Configuring</h6>
<p style="font-size:11px;color:#888;margin-bottom:12px;">Saving ESP32 settings...</p>
<div class="save-bar-track">
<div class="save-bar-fill" style="background:linear-gradient(90deg,#f59e0b,#fbbf24);"></div>
</div>
</div>
<!-- Step 3: Restarting -->
<div id="saveStep3" class="save-step text-center" style="display:none;">
<div class="save-orbit-wrap">
<i class="mdi mdi-restart save-center-icon"
style="color:#ef4444;animation:bounceRestart 1s ease infinite;"></i>
</div>
<h6 class="font-weight-bold mb-1">Restarting ESP32</h6>
<p style="font-size:11px;color:#888;margin-bottom:12px;">Connecting to new network...</p>
<div class="d-flex justify-content-center" style="gap:5px;">
<div class="restart-dot" style="animation-delay:0s;"></div>
<div class="restart-dot" style="animation-delay:0.15s;"></div>
<div class="restart-dot" style="animation-delay:0.3s;"></div>
</div>
</div>
<!-- Step 4: Success -->
<div id="saveStep4" class="save-step text-center" style="display:none;">
<div
style="width:70px;height:70px;margin:0 auto 14px;border-radius:50%;background:rgba(16,185,129,0.1);display:flex;align-items:center;justify-content:center;">
<i class="mdi mdi-check-circle" style="font-size:44px;color:#10b981;animation:popIn 0.5s ease;"></i>
</div>
<h6 class="font-weight-bold mb-1" style="color:#10b981;">Successfully Connected!</h6>
<p id="saveNewSsidDisplay" style="font-size:13px;font-weight:700;color:#333;margin-bottom:14px;"></p>
<button type="button" class="btn btn-sm btn-block" id="btnCloseSaveModal"
style="background:#10b981;color:#fff;border-radius:6px;font-weight:600;font-size:12px;"><i
class="mdi mdi-check mr-1"></i> Done</button>
</div>
<!-- Step 5: Failed -->
<div id="saveStep5" class="save-step text-center" style="display:none;">
<div
style="width:70px;height:70px;margin:0 auto 14px;border-radius:50%;background:rgba(239,68,68,0.1);display:flex;align-items:center;justify-content:center;">
<i class="mdi mdi-close-circle" style="font-size:44px;color:#ef4444;animation:popIn 0.5s ease;"></i>
</div>
<h6 class="font-weight-bold mb-1" style="color:#ef4444;">Connection Failed!</h6>
<p id="saveErrorDisplay" style="font-size:11px;color:#888;margin-bottom:10px;padding:0 10px;">Wrong password
or weak signal.</p>
<!-- Detail Explanation Box (Analisis Kegagalan) -->
<div id="saveFailDetailBox" style="text-align:left;padding:8px 14px;margin-bottom:6px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<i class="mdi mdi-alert-circle" style="font-size:16px;color:#dc2626;"></i>
<span style="font-size:11px;font-weight:700;color:#212024;">Failure Analysis</span>
</div>
<div id="saveFailReasonText" style="font-size:10.5px;color:#212024;line-height:1.6;"></div>
</div>
<!-- Saran Tindakan Box (separate, no background) -->
<div id="saveFailSuggestBox" style="text-align:left;padding:8px 14px;margin-bottom:10px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<i class="mdi mdi-lightbulb-on-outline" style="font-size:14px;color:#d97706;"></i>
<span style="font-size:10.5px;font-weight:700;color:#212024;">Suggested Actions</span>
</div>
<ul id="saveFailSuggestions"
style="font-size:10.5px;color:#212024;line-height:1.6;padding-left:16px;margin:0;"></ul>
</div>
<!-- Fallback Status (outside both boxes) -->
<div id="saveFailFallbackInfo" style="margin-bottom:14px;display:none;"></div>
<button type="button" class="btn btn-sm btn-block" id="btnRetrySaveModal"
style="background:#ef4444;color:#fff;border-radius:6px;font-weight:600;font-size:12px;"><i
class="mdi mdi-refresh mr-1"></i> Try Again</button>
</div>
</div>
</div>
</div>
</div>
<!-- Login History Modal removed - now displayed inline in card -->
<!-- ==================== MODAL: NOTIFICATION ACCESS ==================== -->
<div id="notif-gate"
style="position: fixed; inset: 0; z-index: 99999; background: rgba(15, 23, 42, 0.7); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; padding: 32px; text-align: center;">
<div class="modal-card" id="notif-modal"
style="background: #ffffff; border-radius: 24px; padding: 32px 24px; width: 100%; max-width: 360px; box-shadow: 0 20px 40px rgba(0,0,0,0.15); animation: notifModalPop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); position: relative; z-index: 100000; text-align: center;">
<div class="notif-icon-wrap" id="notif-icon-wrap"
style="width: 80px; height: 80px; border-radius: 50%; background: #e6e6f2; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px; position: relative; transition: all 0.3s;">
<i class="mdi mdi-bell-ring" id="notif-icon" style="font-size: 38px; color: #4747a1; transition: all 0.3s;"></i>
</div>
<h2
style="font-size: 22px; font-weight: 800; color: #0f172a; margin-bottom: 12px; font-family: 'Inter', sans-serif;">
Notification Access</h2>
<p
style="font-size: 14px; color: #64748b; margin-bottom: 28px; line-height: 1.6; font-family: 'Inter', sans-serif;">
Please allow notifications to stay updated in real-time. The system will send instant alerts to your device
whenever a new chat arrives, new occupant or account registrations occur, when the FTM Room Door is accessed,
and if the ESP32 Wi-Fi is disconnected.</p>
<div style="display: flex; flex-direction: column; gap: 12px;">
<button id="btn-allow-notif"
style="background: #4747a1; color: #fff; border: none; padding: 16px; border-radius: 14px; font-weight: 700; font-size: 15px; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif; display: flex; align-items: center; justify-content: center; gap: 8px;">
<i class="mdi mdi-bell-plus"></i> Allow Notification
</button>
<button id="btn-deny-notif"
style="background: transparent; color: #64748b; border: 2px solid #e2e8f0; padding: 14px; border-radius: 14px; font-weight: 700; font-size: 15px; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif;">
Deny
</button>
<button id="btn-close-notif"
style="display: none; background: #4747a1; color: #fff; border: none; padding: 16px; border-radius: 14px; font-weight: 700; font-size: 15px; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif; align-items: center; justify-content: center; gap: 8px;">
<i class="mdi mdi-close-circle-outline" style="font-size: 18px;"></i> Close
</button>
<div id="notif-translate-wrap" style="text-align: right; margin-top: 4px;">
<span id="translate-notif"
style="font-size: 11px; font-weight: 600; color: #4747a1; cursor: pointer; text-decoration: underline;">Terjemahkan
ke Indonesia</span>
</div>
</div>
</div>
</div>
<style>
@keyframes notifModalPop {
0% {
transform: scale(0.8) translateY(20px);
opacity: 0;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.notif-icon-wrap::after {
content: '';
position: absolute;
inset: -8px;
border-radius: 50%;
border: 2px solid rgba(71, 71, 161, 0.3);
animation: pulse-ring 2s infinite;
}
.notif-icon-wrap.blocked {
background: #fee2e2 !important;
}
.notif-icon-wrap.blocked i {
color: #ef4444 !important;
}
.notif-icon-wrap.blocked::after {
border-color: rgba(239, 68, 68, 0.4) !important;
}
@keyframes pulse-ring {
0% {
transform: scale(0.8);
opacity: 1;
}
100% {
transform: scale(1.3);
opacity: 0;
}
}
#btn-allow-notif:hover,
#btn-close-notif:hover {
transform: translateY(-2px);
}
#btn-deny-notif:hover {
background: #f1f5f9;
color: #0f172a;
border-color: #cbd5e1;
}
</style>
<!-- partial:partials/_footer.html -->
<?php include 'footer.php'; ?>
<!-- DASHBOARD SCRIPTS -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script>
$(document).ready(function () {
// ===== NOTIFICATION ACCESS LOGIC =====
var notifTranslated = false;
function checkNotificationAccess() {
if (!("Notification" in window)) return;
// Clear dismiss status if user just logged in (arrived from login page)
if (document.referrer && (document.referrer.includes('login') || document.referrer.includes('index'))) {
sessionStorage.removeItem('notif_modal_dismissed');
}
// We check permission. If granted, never show. If not granted, we show.
// We use sessionStorage to ensure it functions as requested: "MUNCUL LAGI DISETIAP LOGIN BERHASIL" if denied.
if (Notification.permission !== 'granted' && sessionStorage.getItem('notif_modal_dismissed') !== 'true') {
$('#notif-gate').css('display', 'flex');
if (Notification.permission === 'denied') {
$('#notif-gate h2').text(notifTranslated ? 'Notifikasi Diblokir' : 'Notifications Blocked');
$('#notif-gate p').text(notifTranslated ?
'Anda telah memblokir notifikasi untuk situs ini. Agar sistem dapat kembali mengirimkan peringatan real-time terkait pesan baru, pendaftaran akun/occupant, aktivitas akses Pintu Ruang FTM, serta jika koneksi Wi-Fi ESP32 terputus, mohon aktifkan ulang izin notifikasi melalui pengaturan browser Anda.' :
'You have blocked notifications for this site. To resume receiving real-time alerts regarding new messages, account/occupant registrations, FTM Room Door access activities, and ESP32 Wi-Fi disconnections, please re-enable permissions in your browser settings.');
$('#notif-icon-wrap').addClass('blocked');
$('#notif-icon').removeClass('mdi-bell-ring').addClass('mdi-bell-off');
$('#btn-allow-notif').hide();
$('#btn-deny-notif').hide();
$('#btn-close-notif').css('display', 'flex');
}
}
}
setTimeout(checkNotificationAccess, 800);
$('#btn-allow-notif').on('click', function () {
if (!("Notification" in window)) return;
Notification.requestPermission().then(function (permission) {
if (permission === 'granted') {
$('#notif-gate').fadeOut(300);
} else if (permission === 'denied') {
// Jika browser tetap menolak, tampilkan teks peringatan dan ganti tombol.
$('#notif-gate h2').text(notifTranslated ? 'Notifikasi Diblokir' : 'Notifications Blocked');
$('#notif-gate p').text(notifTranslated ?
'Anda telah memblokir notifikasi untuk situs ini. Agar sistem dapat kembali mengirimkan peringatan real-time terkait pesan baru, pendaftaran akun/occupant, aktivitas akses Pintu Ruang FTM, serta jika koneksi Wi-Fi ESP32 terputus, mohon aktifkan ulang izin notifikasi melalui pengaturan browser Anda.' :
'You have blocked notifications for this site. To resume receiving real-time alerts regarding new messages, account/occupant registrations, FTM Room Door access activities, and ESP32 Wi-Fi disconnections, please re-enable permissions in your browser settings.');
$('#notif-icon-wrap').addClass('blocked');
$('#notif-icon').removeClass('mdi-bell-ring').addClass('mdi-bell-off');
$('#btn-allow-notif').hide();
$('#btn-deny-notif').hide();
$('#btn-close-notif').css('display', 'flex');
} else {
sessionStorage.setItem('notif_modal_dismissed', 'true');
$('#notif-gate').fadeOut(300);
}
});
});
$('#btn-deny-notif').on('click', function () {
sessionStorage.setItem('notif_modal_dismissed', 'true');
$('#notif-gate').fadeOut(300);
});
$('#btn-close-notif').on('click', function () {
sessionStorage.setItem('notif_modal_dismissed', 'true');
$('#notif-gate').fadeOut(300);
});
$('#translate-notif').on('click', function () {
var isBlocked = $('#notif-icon-wrap').hasClass('blocked');
if (!notifTranslated) {
if (isBlocked) {
$('#notif-gate h2').text('Notifikasi Diblokir');
$('#notif-gate p').text('Anda telah memblokir notifikasi untuk situs ini. Agar sistem dapat kembali mengirimkan peringatan real-time terkait pesan baru, pendaftaran akun/occupant, aktivitas akses Pintu Ruang FTM, serta jika koneksi Wi-Fi ESP32 terputus, mohon aktifkan ulang izin notifikasi melalui pengaturan browser Anda.');
} else {
$('#notif-gate h2').text('Akses Notifikasi');
$('#notif-gate p').text('Mohon aktifkan notifikasi agar Anda dapat menerima pemberitahuan secara real-time. Sistem akan memberikan informasi langsung ke perangkat Anda setiap kali terdapat pesan masuk, aktivitas pendaftaran akun/occupant baru, saat Pintu Ruang FTM diakses, maupun jika koneksi Wi-Fi ESP32 terputus.');
}
$(this).text('Translate to Engl ish');
notifTranslated = true;
} else {
if (isBlocked) {
$('#notif-gate h2').text('Notifications Blocked');
$('#notif-gate p').text('You have blocked notifications for this site. To resume receiving real-time alerts regarding new messages, account/occupant registrations, FTM Room Door access activities, and ESP32 Wi-Fi disconnections, please re-enable permissions in your browser settings.');
} else {
$('#notif-gate h2').text('Notification Access');
$('#notif-gate p').text('Please allow notifications to stay updated in real-time. The system will send instant alerts to your device whenever a new chat arrives, new occupant or account registrations occur, when the FTM Room Door is accessed, and if the ESP32 Wi-Fi is disconnected.');
}
$(this).text('Terjemahkan ke Indonesia');
notifTranslated = false;
}
});
// ===== LIVE CLOCK & DATE =====
function updateDashClock() {
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var s = String(now.getSeconds()).padStart(2, '0');
$('#dashLiveClock').text(h + ':' + m + ':' + s);
var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var hari = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
var IDbulan = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
$('#dashLiveDate').text(now.getDate() + ' ' + months[now.getMonth()] + ' ' + now.getFullYear());
}
updateDashClock();
setInterval(updateDashClock, 1000);
// ===== LOGIN HISTORY INLINE CARD =====
function loadLoginHistoryCard() {
$.getJSON('api_login_history.php', function (res) {
if (res.success && res.logs.length > 0) {
var html = '';
res.logs.forEach(function (log, i) {
var iconBg = log.is_desktop ? '#eff6ff' : '#f0fdf4';
var iconColor = log.is_desktop ? '#3b82f6' : '#10b981';
var deviceIcon = log.is_desktop ? 'mdi-monitor' : 'mdi-cellphone';
html += '<div class="d-flex align-items-center py-2 px-2" style="border-bottom:1px solid #f1f5f9;transition:background 0.2s;border-radius:6px;margin-bottom:2px;" onmouseenter="this.style.background=\'#f8fafc\'" onmouseleave="this.style.background=\'\'">';
html += ' <div style="width:32px;height:32px;border-radius:50%;background:' + iconBg + ';display:flex;align-items:center;justify-content:center;margin-right:10px;flex-shrink:0;">';
html += ' <i class="mdi ' + deviceIcon + '" style="font-size:15px;color:' + iconColor + ';"></i>';
html += ' </div>';
html += ' <div style="flex:1;min-width:0;">';
html += ' <div style="font-size:11px;font-weight:600;color:#212529;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + log.device + (log.browser ? ' · ' + log.browser : '') + '</div>';
html += ' <div style="font-size:10px;color:#94a3b8;">' + log.date + ' · ' + log.time + '</div>';
html += ' </div>';
html += ' <div style="flex-shrink:0;text-align:right;">';
html += ' <span style="font-size:10px;font-family:monospace;color:#4B49AC;font-weight:600;">' + log.ip + '</span>';
html += ' </div>';
html += '</div>';
});
html += '<div style="text-align:center;padding:6px 0 2px;font-size:10px;color:#cbd5e1;"><i class="mdi mdi-information-outline mr-1"></i>' + res.logs.length + ' login records</div>';
$('#loginHistoryCardBody').html(html);
} else if (res.success && res.logs.length === 0) {
$('#loginHistoryCardBody').html('<div class="text-center py-4"><i class="mdi mdi-history" style="font-size:36px;color:#e2e8f0;"></i><p class="mt-2 mb-0" style="font-size:11px;color:#999;">No login history found.</p></div>');
} else {
$('#loginHistoryCardBody').html('<div class="text-center py-3 text-danger"><i class="mdi mdi-alert-circle" style="font-size:24px;"></i><p class="mt-1 mb-0" style="font-size:11px;">Failed to load.</p></div>');
}
}).fail(function () {
$('#loginHistoryCardBody').html('<div class="text-center py-3 text-danger"><i class="mdi mdi-alert-circle" style="font-size:24px;"></i><p class="mt-1 mb-0" style="font-size:11px;">Connection error.</p></div>');
});
}
loadLoginHistoryCard();
// Password toggle removed as requested
// ===== WIFI MODAL PASSWORD TOGGLE =====
$('#toggleNewPass').on('click', function () {
var inp = $('#new-wifi-pass');
if (inp.attr('type') === 'password') {
inp.attr('type', 'text');
$(this).find('i').removeClass('mdi-eye-outline').addClass('mdi-eye-off-outline');
} else {
inp.attr('type', 'password');
$(this).find('i').removeClass('mdi-eye-off-outline').addClass('mdi-eye-outline');
}
});
// ===== WIFI SCAN BUTTON =====
$('#btnScanWifi').on('click', function () {
$('#wifiScanAnim').show();
$('#wifiNetworkList').hide();
$('#wifiListItems').empty();
$('#wifiScanModal').modal('show');
// Trigger Scan on Backend
$.post('api_wifi.php', { action: 'trigger_scan' }, function (triggerRes) {
if (triggerRes.success) {
// Poll for results every 1.5 seconds
var scanPoll = setInterval(function () {
$.post('api_wifi.php', { action: 'get_scan_results' }, function (res) {
if (res.success && res.results) {
clearInterval(scanPoll); // Stop polling
$('#wifiScanAnim').hide();
var html = '';
if (res.results.length === 0) {
html = '<div class="text-center p-3 text-muted" style="font-size:12px;">No WiFi networks detected.</div>';
} else {
res.results.forEach(function (net) {
// Convert RSSI to roughly percentage (RSSI goes from approx -30 to -90)
var rssi = parseInt(net.signal) || -90;
var signalPct = Math.min(Math.max(2 * (rssi + 100), 0), 100);
var sigIcon = signalPct > 70 ? 'mdi-wifi-strength-4' : (signalPct > 40 ? 'mdi-wifi-strength-2' : 'mdi-wifi-strength-1');
var sigColor = signalPct > 70 ? '#10b981' : (signalPct > 40 ? '#f59e0b' : '#ef4444');
var isLocked = (net.locked === true || net.locked === "true" || net.locked === 1 || net.locked === "1");
html += '<div class="wifi-network-item d-flex align-items-center justify-content-between" style="padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:6px;cursor:pointer;transition:background 0.2s;" data-ssid="' + net.ssid + '">';
html += ' <div class="d-flex align-items-center">';
html += ' <i class="mdi ' + sigIcon + '" style="font-size:20px;color:' + sigColor + ';margin-right:10px;"></i>';
html += ' <div>';
html += ' <span style="font-size:12px;font-weight:600;color:#212529;">' + net.ssid + '</span>';
html += ' <br><span style="font-size:10px;color:#999;">' + signalPct + '% signal (' + rssi + ' dBm)</span>';
html += ' </div>';
html += ' </div>';
html += ' <i class="mdi ' + (isLocked ? 'mdi-lock' : 'mdi-lock-open-variant') + '" style="font-size:14px;color:' + (isLocked ? '#64748b' : '#10b981') + ';"></i>';
html += '</div>';
});
}
$('#wifiListItems').html(html);
$('#wifiNetworkList').fadeIn(300);
}
}, 'json');
}, 1500);
// Timeout scan after 30 seconds
setTimeout(function () {
clearInterval(scanPoll);
if ($('#wifiScanAnim').is(':visible')) {
$('#wifiScanAnim').hide();
$('#wifiListItems').html('<div class="text-center p-3 text-danger" style="font-size:12px;">Failed to scan networks (Timeout). Make sure the ESP32 is powered on.</div>');
$('#wifiNetworkList').fadeIn(300);
}
}, 30000);
} else {
$('#wifiScanAnim').hide();
$('#wifiListItems').html('<div class="text-center p-3 text-danger" style="font-size:12px;">Failed to trigger scan.</div>');
$('#wifiNetworkList').fadeIn(300);
}
}, 'json').fail(function () {
$('#wifiScanAnim').hide();
$('#wifiListItems').html('<div class="text-center p-3 text-danger" style="font-size:12px;">Server connection error.</div>');
$('#wifiNetworkList').fadeIn(300);
});
});
// Click on network to fill SSID
$(document).on('click', '.wifi-network-item', function () {
var ssid = $(this).data('ssid');
$('#new-ssid').val(ssid);
$('#wifiScanModal').modal('hide');
$('#new-wifi-pass').val('').focus();
});
// Hover effect on network items
$(document).on('mouseenter', '.wifi-network-item', function () {
$(this).css('background', '#f0eeff');
}).on('mouseleave', '.wifi-network-item', function () {
$(this).css('background', '');
});
// Global variable to hold the interval
window.wifiChangeCheckInterval = null;
// ===== WIFI SAVE WITH FULLSCREEN ANIMATION =====
$('#saveWifiBtn').on('click', function () {
var ssid = $('#new-ssid').first().val().trim();
var pass = $('#new-wifi-pass').first().val().trim();
if (!ssid) { alert('SSID cannot be empty'); return; }
// Clear any orphaned polling interval if the user clicks Save rapidly
if (window.wifiChangeCheckInterval) {
clearInterval(window.wifiChangeCheckInterval);
window.wifiChangeCheckInterval = null;
}
// Send change API command
$.post('api_wifi.php', { action: 'change_wifi', ssid: ssid, password: pass }, function (res) {
if (res.success) {
// Reset all steps
$('.save-step').hide();
$('.save-bar-fill').css('width', '0%');
// Reset modal to small for non-failure steps
$('#wifiSaveModalDialog').removeClass('modal-lg').addClass('modal-sm');
$('#saveStep1').show();
$('#wifiSaveModal').modal('show');
// Step 1: Connecting (0-2.5s)
setTimeout(function () { $('#saveStep1 .save-bar-fill').css('width', '50%'); }, 500);
setTimeout(function () { $('#saveStep1 .save-bar-fill').css('width', '100%'); }, 1800);
// Step 2 & 3: Waiting logic
setTimeout(function () {
$('.save-step').hide();
$('#saveStep2').fadeIn(300);
// Animasi progress bar step 2 melambat (pura-pura proses hingga batas waktu)
$('#saveStep2 .save-bar-fill').css({ 'width': '85%', 'transition': 'width 30s cubic-bezier(0.1, 0.8, 0.3, 1)' });
}, 1800);
// Simpan sesi ganti WiFi & Mulai Timer Timeout (Misal 60 Detik maksimal)
var isCheckingChangeStatus = true;
var maxTimeout = 120; // detik (diperpanjang agar cukup untuk connect 15s + fallback 10s + report)
var elapsedWait = 0;
// Polling status penggantian WiFi
window.wifiChangeCheckInterval = setInterval(function () {
elapsedWait += 2;
$.get('api_wifi.php?action=check_change_status', function (statusRes) {
if (statusRes.status === 'failed' || elapsedWait >= maxTimeout) {
clearInterval(window.wifiChangeCheckInterval);
var reason = statusRes.reason || '';
var errMsg = statusRes.status === 'failed' ? (statusRes.message || 'Connection failed.') : 'Connection Timeout.';
// Build detailed failure explanation
var reasonText = '';
var suggestions = [];
var fallbackHtml = '';
var targetSsid = $('#new-ssid').first().val().trim();
if (reason.indexOf('wrong_password') !== -1) {
errMsg = 'WiFi Authentication Rejected (Wrong Password)';
reasonText = '<strong>Cause:</strong> The ESP32 successfully found the WiFi network <strong>"' + targetSsid + '"</strong>, but the router <strong>rejected the connection because the submitted password does not match</strong> the password configured on the access point/router.';
suggestions = [
'<strong>Re-enter the password correctly</strong> — make sure there are no typos, pay attention to uppercase/lowercase (case-sensitive), spaces, and special characters like @, #, !, etc.',
'<strong>Check WiFi security type</strong> — ESP32 only supports WPA and WPA2. If the router uses WPA3 or WPA2/WPA3 mixed mode, change to WPA2-Only in the router settings.',
'<strong>Confirm the latest password</strong> — if the WiFi password was recently changed by the network admin, request the correct password and re-enter it.',
'<strong>Test from another device</strong> — try the same password from a phone/laptop to verify the password is correct before trying again on the ESP32.'
];
} else if (reason.indexOf('no_ssid') !== -1) {
errMsg = 'WiFi Network Not Found (SSID Not Found)';
reasonText = '<strong>Cause:</strong> The ESP32 <strong>failed to find a WiFi network named "' + targetSsid + '"</strong> while scanning the 2.4GHz frequency. The router may be powered off, out of range, or the SSID is set to hidden mode.';
suggestions = [
'<strong>Make sure the target WiFi router is powered on</strong> — check if the WiFi indicator light on the router is blinking/active. If the router is off, turn it on first.',
'<strong>Move ESP32 closer to the router</strong> — being too far away or blocked by thick walls can prevent the signal from being detected. Ideally within <10 meters without obstructions.',
'<strong>Check the SSID spelling</strong> — the WiFi name must be typed exactly the same including uppercase/lowercase. Example: "WiFi_Home" ≠ "wifi_home" ≠ "WiFi Home".',
'<strong>Disable hidden SSID</strong> — if the SSID is hidden (hidden network) in the router settings, enable SSID broadcast first so the ESP32 can detect it.',
'<strong>Make sure the 2.4GHz band is active</strong> — ESP32 only supports the 2.4GHz frequency. If the router only broadcasts 5GHz, the ESP32 will not be able to detect it.'
];
} else if (reason.indexOf('weak_signal') !== -1) {
errMsg = 'WiFi Signal Too Weak (Weak Signal)';
reasonText = '<strong>Cause:</strong> The ESP32 detected the network <strong>"' + targetSsid + '"</strong> but the <strong>signal strength is too weak</strong> (below -75 dBm) to establish a stable connection. Handshake data packets failed to be sent/received properly due to excessive distance or physical obstructions between the ESP32 and router.';
suggestions = [
'<strong>Move ESP32 closer to the target WiFi router</strong> — position the ESP32 within 5-10 meters of the router without major obstructions. Ideal signal strength is above -65 dBm.',
'<strong>Remove physical obstructions</strong> — concrete walls, metal cabinets, and mirrors can significantly absorb or reflect 2.4GHz WiFi signals. Position the ESP32 in a location with a clear line of sight to the router.',
'<strong>Use a WiFi repeater/extender</strong> — if the ESP32 must be placed far from the router, install a WiFi repeater at a midpoint to extend the signal range.',
'<strong>Check the router antenna orientation</strong> — make sure the router antennas are pointing vertically (upright). For multi-floor coverage, tilt one antenna horizontal and one vertical.',
'<strong>Reduce interference</strong> — keep the ESP32 and router away from electronic devices that generate interference such as microwaves, cordless phones, or active Bluetooth devices.'
];
} else if (reason.indexOf('timeout') !== -1 || elapsedWait >= maxTimeout) {
errMsg = 'Connection Timeout';
reasonText = '<strong>Cause:</strong> The ESP32 tried to connect to the network <strong>"' + targetSsid + '"</strong> for <strong>15 seconds but received no response</strong> from the router. The connection could not be established within the specified time limit.';
suggestions = [
'<strong>Check MAC filtering on the router</strong> — some routers restrict device connections based on MAC address. Add the ESP32 MAC address to the whitelist if this feature is enabled.',
'<strong>Restart the target WiFi router</strong> — a router that has been running for a long time may experience memory leaks or channel congestion. A restart can resolve this issue.',
'<strong>Reduce connected devices</strong> — if the router has reached its maximum connected device limit (typically 32 devices), disconnect some unused devices.',
'<strong>Try changing the router WiFi channel</strong> — interference from neighboring WiFi on the same channel can disrupt connections. Switch to channel 1, 6, or 11 with the least interference.',
'<strong>Check firewall and client isolation</strong> — some routers have AP Isolation or Client Isolation features that prevent new devices from connecting. Disable this feature in the router settings.'
];
} else {
errMsg = 'Connection Failed (Unknown Reason)';
reasonText = '<strong>Cause:</strong> The ESP32 failed to connect to the network <strong>"' + targetSsid + '"</strong> due to <strong>a reason that could not be specifically identified</strong> by the microcontroller firmware.';
suggestions = [
'<strong>Restart ESP32</strong> — press the reset button or unplug and reconnect the ESP32 power, then try connecting again.',
'<strong>Re-scan networks</strong> — press the Scan button and select the target WiFi network again, make sure the network is still active and detected.',
'<strong>Check router configuration</strong> — make sure the router is not in maintenance mode, firmware is not being updated, and there is no firewall configuration blocking new devices.',
'<strong>Factory reset if recurring</strong> — if the problem persists, try resetting the ESP32 to factory settings and reconfigure from scratch.'
];
}
// Fallback status info (rendered OUTSIDE the red/yellow boxes)
if (reason.indexOf('fallback_ok') !== -1) {
fallbackHtml = '<div style="background:#f0fdf4;border:1px solid #bbf7d0;color:#166534;padding:10px 12px;border-radius:8px;font-size:10px;line-height:1.5;display:flex;align-items:flex-start;gap:8px;"><i class="mdi mdi-check-circle" style="font-size:16px;color:#16a34a;flex-shrink:0;margin-top:1px;"></i><div><strong>Connection Restored:</strong> The ESP32 has successfully reconnected to the previously connected WiFi network. The system is back online as before.</div></div>';
} else if (reason.indexOf('fallback_fail') !== -1) {
fallbackHtml = '<div style="background:#fef3c7;border:1px solid #fcd34d;color:#92400e;padding:10px 12px;border-radius:8px;font-size:10px;line-height:1.5;display:flex;align-items:flex-start;gap:8px;"><i class="mdi mdi-access-point-network" style="font-size:16px;color:#d97706;flex-shrink:0;margin-top:1px;"></i><div><strong>Hotspot Mode Active:</strong> The ESP32 also failed to reconnect to the previous WiFi. Currently, the ESP32 is broadcasting a configuration hotspot. Connect to WiFi <strong>@Identia.id</strong> then open <strong>192.168.4.1</strong> in a browser for manual configuration.</div></div>';
}
// Render into modal
$('.save-step').hide();
$('#saveStep2 .save-bar-fill').css({ 'width': '0%', 'transition': 'none' });
$('#saveErrorDisplay').text(errMsg);
$('#saveFailReasonText').html(reasonText);
var sugHtml = '';
suggestions.forEach(function (s, i) { sugHtml += '<li style="margin-bottom:4px;">' + s + '</li>'; });
$('#saveFailSuggestions').html(sugHtml);
if (fallbackHtml) {
$('#saveFailFallbackInfo').html(fallbackHtml).show();
} else {
$('#saveFailFallbackInfo').hide();
}
// Widen modal for detailed failure view
$('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');
$('#saveStep5').fadeIn(300);
} else if (statusRes.status === 'success') {
clearInterval(window.wifiChangeCheckInterval);
$('#saveStep2 .save-bar-fill').css({ 'width': '100%', 'transition': 'width 0.5s' }); // Cepat penuhi
setTimeout(function () {
// Lanjutkan ke Success View
$('.save-step').hide();
$('#saveStep2 .save-bar-fill').css({ 'width': '0%', 'transition': 'none' }); // Reset for next
$('#saveNewSsidDisplay').html('<i class="mdi mdi-wifi mr-1"></i> ' + ssid);
$('#saveStep4').fadeIn(300);
// Perbarui kartu ESP32 di Dashboard
$('#esp32-ssid').text(ssid);
// Trigger Notification
$.post('api_notifications.php', {
action: 'create_notification',
type: 'wifi_change',
title: 'WiFi Connected',
message: 'Successfully switched to WiFi: ' + ssid
});
}, 600);
}
}, 'json').fail(function () {
// Jika API API PHP gak bisa diakses (misal server mati)
if (elapsedWait >= maxTimeout) {
clearInterval(window.wifiChangeCheckInterval);
$('.save-step').hide();
$('#saveStep2 .save-bar-fill').css({ 'width': '0%', 'transition': 'none' }); // Reset
$('#saveErrorDisplay').text('Connection error to the PHP API server.');
$('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');
$('#saveStep5').fadeIn(300);
}
});
}, 2000); // Check every 2 seconds (faster feedback)
} else {
alert("Failed to save: " + res.message);
}
}, 'json');
});
// Close fullscreen save modal and refresh on OK
$('#btnCloseSaveModal').on('click', function () {
$('#wifiSaveModal').modal('hide');
// location.reload(); // Optional: reload page to ensure clean state
});
// Retry button on failed modal
$('#btnRetrySaveModal').on('click', function () {
$('#wifiSaveModal').modal('hide');
});
// SIMULATE WIFI DISCONNECT (Toggle locally for test, but actual logic relies on polling below)
var isWifiConnected = true;
var hasNotifiedWifiDisconnect = false;
var hasNotifiedWifiConnect = true; // start true so it doesn't notify on first load if already connected
$('#wifi-status-text').closest('p').css('cursor', 'pointer').on('click', function () {
// Toggle for visual testing ONLY
$(this).toggleClass('manual-override');
});
// ===== Real-time ESP32 Status Polling =====
function pollESP32Status() {
$.post('api_wifi.php', { action: 'get_status' }, function (res) {
if ($('#wifi-status-text').closest('p').hasClass('manual-override')) return; // skip if manually toggled
if (res.status === 'online') {
if (!isWifiConnected || $('#wifi-status-text').text() !== 'WiFi Connected') {
isWifiConnected = true;
$('#wifi-status-text').text('WiFi Connected').css('color', '#ffffff');
$('#wifi-dot').css({ 'background': '#10b981', 'box-shadow': 'none' });
// Trigger notification on reconnect
if (!hasNotifiedWifiConnect && hasNotifiedWifiDisconnect) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-wifi',
title: 'WiFi Terhubung',
message: '@Identia.id berhasil terhubung kembali ke jaringan WiFi.',
detail: 'Sistem kembali online dan dapat dipantau.',
is_danger: 0
});
hasNotifiedWifiConnect = true;
hasNotifiedWifiDisconnect = false;
}
}
// Update RSSI Signal Strength Icon
var rssi = parseInt(res.rssi) || -90;
var signalPct = Math.min(Math.max(2 * (rssi + 100), 0), 100);
var sigIcon = signalPct > 70 ? 'mdi-wifi-strength-4' : (signalPct > 40 ? 'mdi-wifi-strength-2' : 'mdi-wifi-strength-1');
var sigColor = signalPct > 70 ? '#10b981' : (signalPct > 40 ? '#f59e0b' : '#ef4444');
$('#esp32-status-icon-container').html('<i class="mdi ' + sigIcon + '" style="font-size:24px;color:' + sigColor + ';" title="Signal: ' + signalPct + '%"></i>');
// Selalu Update connected SSID
if (res.current_ssid && res.current_ssid !== 'Unknown') {
$('#esp32-ssid').text(res.current_ssid);
}
// Selalu Update IP Address
if (res.esp_ip && res.esp_ip !== 'Unknown') {
$('#esp32-ip').text(res.esp_ip);
}
} else if (res.status === 'connecting') {
isWifiConnected = false;
$('#wifi-status-text').text('WiFi Connecting...').css('color', '#ffffff');
$('#wifi-dot').css({ 'background': '#f59e0b', 'box-shadow': 'none' });
// Loading Spinner
$('#esp32-status-icon-container').html('<i class="mdi mdi-loading mdi-spin" style="font-size:24px;color:#fcd34d;"></i>');
$('#esp32-ssid').text('Unknown');
$('#esp32-ip').text('Unknown');
} else {
// Offline
if (isWifiConnected || $('#wifi-status-text').text() !== 'WiFi Disconnected') {
isWifiConnected = false;
$('#wifi-status-text').text('WiFi Disconnected').css('color', '#ffffff');
$('#wifi-dot').css({ 'background': '#ef4444', 'box-shadow': 'none' });
$('#esp32-status-icon-container').html('<i class="mdi mdi-wifi-off" style="font-size:24px;color:#cbd5e1;"></i>');
$('#esp32-ssid').text('Unknown');
$('#esp32-ip').text('Unknown');
// Trigger danger notification on disconnect
if (!hasNotifiedWifiDisconnect) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-wifi-off',
title: 'WiFi Terputus',
message: 'Koneksi ke ESP32 terputus! Alat sedang dalam status offline.',
detail: 'Segera periksa ketersediaan internet pada router atau sumber daya pada Smart Door.',
is_danger: 1
});
hasNotifiedWifiDisconnect = true;
hasNotifiedWifiConnect = false;
}
}
}
}, 'json').fail(function () {
console.error("Failed to poll ESP32 status");
});
}
// Poll every 3 seconds
setInterval(pollESP32Status, 3000);
pollESP32Status();
// ===== Real-time Door Status Polling =====
function pollDoorStatus() {
$.get('api_door_status.php', function (data) {
if (data && data.status === 'success') {
// Update status text and icon based on server state
if (data.door_state === 'unlocked') {
$('#dash-top-door-pill').html('<i class="mdi mdi-lock-open mr-1"></i>UNLOCKED')
.css({ 'background': '#ffffff', 'color': '#dc2626', 'font-weight': '700', 'box-shadow': '0 2px 4px rgba(0,0,0,0.05)' });
} else {
$('#dash-top-door-pill').html('<i class="mdi mdi-lock mr-1"></i>LOCKED')
.css({ 'background': '#ffffff', 'color': '#059669', 'font-weight': '700', 'box-shadow': '0 2px 4px rgba(0,0,0,0.05)' });
}
// Update physical time strings
if (data.last_updated_time) {
$('#dash-top-door-time').text(data.last_updated_time);
}
if (data.last_updated_date) {
$('#dash-top-door-date').text(data.last_updated_date);
}
}
}, 'json');
}
// Poll exactly as access.php does (every 2.5s)
setInterval(pollDoorStatus, 2500);
pollDoorStatus();
// SIMULATE BATTERY FULL / CHARGING TOGGLE
var isCharging = false;
var batFullTimestamp = null;
var hasNotifiedLowBat = false;
var hasNotifiedAlmostFull = false;
var hasNotifiedFull = false;
var hasNotifiedOvercharge = false;
$('#battery-percent-wrap').css('cursor', 'pointer').attr('title', 'Click to toggle Charging / Discharging').on('click', function () {
isCharging = !isCharging;
if (isCharging) {
batLevel = 100;
batFullTimestamp = new Date().getTime();
hasNotifiedAlmostFull = true;
hasNotifiedFull = true;
$('#bat-status').text('Charging').css('color', '#3b82f6');
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-battery-charging-100',
title: 'Smart Door Battery Full',
message: 'Kapasitas baterai telah mencapai 100%.',
detail: 'Pengisian daya sudah penuh.',
is_danger: 0
});
alert('Device is now CHARGING (Simulated at 100%). Menunggu 1 jam untuk peringatan overcharge...');
} else {
batFullTimestamp = null;
hasNotifiedOvercharge = false;
hasNotifiedFull = false;
$('#bat-status').text('Discharging').css('color', '#10b981');
alert('Device is now DISCHARGING.');
}
});
// ===== BATTERY MONITORING CHART =====
var maxPts = 40, batLevel = 85, batLabels = [], batData = [];
var now = new Date();
for (var i = maxPts - 1; i >= 0; i--) {
var t = new Date(now.getTime() - i * 2000);
batLabels.push(t.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' }));
batData.push(batLevel + Math.random() * 2 - 1);
}
var ctx = document.getElementById('battery-chart').getContext('2d');
var grad = ctx.createLinearGradient(0, 0, 0, 200);
grad.addColorStop(0, 'rgba(75,73,172,0.25)');
grad.addColorStop(1, 'rgba(75,73,172,0.02)');
var batChart = new Chart(ctx, {
type: 'line',
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' }] },
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 + '%'; } } } } }
});
setInterval(function () {
if (!isCharging) {
batLevel -= (Math.random() * 0.5 + 0.1);
// Auto charge if too low (for simulation loops)
if (batLevel < 5) {
isCharging = true;
$('#bat-status').text('Charging').css('color', '#3b82f6');
hasNotifiedLowBat = false;
}
} else {
if (batLevel < 100) batLevel += (Math.random() * 2 + 1);
if (batLevel > 100) batLevel = 100;
}
// Battery Overcharge Logic (100% for 1 hour)
if (batLevel === 100 && isCharging) {
if (!batFullTimestamp) batFullTimestamp = new Date().getTime();
if (!hasNotifiedFull) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-battery-charging-100',
title: 'Smart Door Battery Full',
message: 'Kapasitas baterai telah terisi 100%.',
detail: 'Pengisian daya telah selesai dilakukan.',
is_danger: 0
});
hasNotifiedFull = true;
}
// 1 hour = 3600000 ms
if (!hasNotifiedOvercharge && (new Date().getTime() - batFullTimestamp) > 3600000) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-battery-alert',
title: 'Overcharge',
message: 'Baterai di tingkat 100% dan charger mendeteksi terhubung selama 1 jam lebih.',
detail: 'Sistem menyarankan agar pengisi daya segera dicabut untuk menghindari kerusakan pada baterai.',
is_danger: 1
});
hasNotifiedOvercharge = true;
}
} else {
batFullTimestamp = null;
hasNotifiedOvercharge = false;
hasNotifiedFull = false;
}
// Low Battery Notification (<= 20%)
if (batLevel <= 20 && !hasNotifiedLowBat && !isCharging) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-battery-10',
title: 'Low Baterai',
message: 'Kapasitas tersisa di bawah 20%.',
detail: 'Segera hubungkan charger untuk menjaga sistem door lock.',
is_danger: 1
});
hasNotifiedLowBat = true;
} else if (batLevel > 25) {
hasNotifiedLowBat = false;
}
// Almost Full Notification (>= 90% and < 100%)
if (batLevel >= 90 && batLevel < 100 && !hasNotifiedAlmostFull && isCharging) {
$.post('api_notifications.php', {
action: 'create_notification',
icon: 'mdi-battery-90',
title: 'Baterai Hampir Penuh',
message: 'Kapasitas alat sudah mencapai ' + Math.floor(batLevel) + '%.',
detail: 'Tidak lama lagi bisa dicabut dari pengisian daya.',
is_danger: 0
});
hasNotifiedAlmostFull = true;
} else if (batLevel < 85) {
hasNotifiedAlmostFull = false;
}
var val = Math.max(0, Math.min(100, batLevel));
var t2 = new Date();
batLabels.push(t2.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' }));
batData.push(val);
if (batLabels.length > maxPts) { batLabels.shift(); batData.shift(); }
batChart.update();
var pct = Math.round(val);
var circ = 2 * Math.PI * 52;
var off = circ - (circ * pct / 100);
var gf = document.getElementById('gauge-fill');
gf.setAttribute('stroke-dashoffset', off);
document.getElementById('gauge-text').textContent = pct + '%';
var c = '#10b981';
if (pct <= 20) c = '#ef4444'; else if (pct <= 60) c = '#f59e0b';
gf.setAttribute('stroke', c);
// Update voltage visually
if (!isCharging) {
$('#bat-voltage').text((3.0 + (val / 100) * 1.2).toFixed(2) + 'V');
} else {
$('#bat-voltage').text('5.00V');
}
}, 2000);
// ===== LED RGB COLOR PICKERS =====
$('.led-color-picker').on('input', function () {
var clr = $(this).val();
var tid = $(this).data('target');
var nid = $(this).data('name');
$('#' + tid).css('background', clr);
$('#' + nid).text(clr.toUpperCase());
});
});
</script>
<style>
/* === WiFi Antenna Arc Animation === */
.wifi-arc {
opacity: 0.12;
}
.arc-l1,
.arc-r1 {
animation: arcBlink 2s ease-in-out infinite 0s;
}
.arc-l2,
.arc-r2 {
animation: arcBlink 2s ease-in-out infinite 0.3s;
}
.arc-l3,
.arc-r3 {
animation: arcBlink 2s ease-in-out infinite 0.6s;
}
.arc-l4,
.arc-r4 {
animation: arcBlink 2s ease-in-out infinite 0.9s;
}
@keyframes arcBlink {
0%,
100% {
opacity: 0.12;
}
20%,
45% {
opacity: 1;
}
}
.scan-dots {
animation: scanDots 1.5s steps(4, end) infinite;
}
@keyframes scanDots {
0% {
content: '';
}
25% {
content: '.';
}
50% {
content: '..';
}
75% {
content: '...';
}
}
/* === Save Modal Glass Card === */
.save-glass-card {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
padding: 28px 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.3) inset;
}
#wifiSaveModal .modal-backdrop {
opacity: 0.35 !important;
}
/* === Orbit Animation (Step 1) === */
.save-orbit-wrap {
position: relative;
width: 80px;
height: 80px;
margin: 0 auto 16px;
}
.save-center-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
}
.save-orbit-dot {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #4B49AC;
top: 50%;
left: 50%;
animation: orbitSpin 1.5s linear infinite;
animation-delay: var(--delay, 0s);
}
@keyframes orbitSpin {
0% {
transform: translate(-50%, -50%) rotate(0deg) translateX(34px) scale(1);
opacity: 0.8;
}
50% {
opacity: 1;
transform: translate(-50%, -50%) rotate(180deg) translateX(34px) scale(1.3);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(34px) scale(1);
opacity: 0.8;
}
}
/* === Particles (Step 2) === */
.save-particle {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
background: #f59e0b;
top: 50%;
left: 50%;
animation: particleFly 2s ease-in-out infinite;
animation-delay: var(--delay, 0s);
}
@keyframes particleFly {
0% {
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(0) scale(0);
opacity: 0;
}
30% {
opacity: 1;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(30px) scale(1);
}
70% {
opacity: 0.6;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(38px) scale(0.6);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(42px) scale(0);
}
}
/* === Bounce Restart (Step 3) === */
@keyframes bounceRestart {
0%,
100% {
transform: translate(-50%, -50%) scale(1) rotate(0deg);
}
25% {
transform: translate(-50%, -50%) scale(0.85) rotate(-30deg);
}
50% {
transform: translate(-50%, -50%) scale(1.15) rotate(15deg);
}
75% {
transform: translate(-50%, -50%) scale(0.95) rotate(-5deg);
}
}
.restart-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #ef4444;
animation: dotPulse 1.2s ease-in-out infinite;
}
/* === Shared Keyframes === */
@keyframes spinSave {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes dotPulse {
0%,
80%,
100% {
transform: scale(0.5);
opacity: 0.3;
}
40% {
transform: scale(1.2);
opacity: 1;
}
}
@keyframes popIn {
0% {
transform: scale(0);
opacity: 0;
}
60% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* === Progress Bar === */
.save-bar-track {
width: 160px;
height: 4px;
background: #eee;
border-radius: 4px;
margin: 0 auto;
overflow: hidden;
}
.save-bar-fill {
width: 0%;
height: 100%;
border-radius: 4px;
transition: width 0.6s ease;
}
</style>