TKK_E32230273/192.168.4.1.php

325 lines
14 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<title>Identia Connect Portal</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
<style>
:root {
--primary: #4747a1;
--primary-light: #e6e6f2;
--primary-glow: rgba(71,71,161,0.3);
--bg: #f8fafc;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #f0f4f8, var(--bg));
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.portal-card {
background: var(--card);
border-radius: 24px;
width: 100%; max-width: 400px;
padding: 40px 30px;
box-shadow: 0 20px 50px rgba(0,0,0,0.08);
text-align: center;
position: relative;
overflow: hidden;
border: 1px solid var(--border);
}
.icon-wrap {
width: 90px; height: 90px; border-radius: 50%;
background: var(--primary-light);
display: flex; align-items: center; justify-content: center;
margin: 0 auto 24px;
position: relative;
}
.icon-wrap.pulsing::before {
content: ''; position: absolute; inset: -10px; border-radius: 50%;
border: 2px solid var(--primary-glow);
animation: pulse-ring 2s infinite;
}
.icon-wrap.active { background: #dcfce7; }
.icon-wrap.active::before { border-color: #86efac; }
.icon-wrap.error { background: #fef2f2; }
.icon-wrap.error::before { border-color: #fecaca; }
@keyframes pulse-ring { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }
.icon-wrap i { font-size: 44px; color: var(--primary); transition: color 0.3s; }
.icon-wrap.active i { color: #10b981; }
.icon-wrap.error i { color: #ef4444; }
.portal-card h1 { font-size: 24px; font-weight: 800; color: var(--text); margin-bottom: 8px; letter-spacing: -0.5px; }
.portal-card p.desc { font-size: 14px; color: var(--muted); margin-bottom: 32px; line-height: 1.6; }
.btn-connect {
background: var(--primary); color: #fff;
border: none; border-radius: 16px;
width: 100%; padding: 16px; font-size: 15px; font-weight: 700;
cursor: pointer; font-family: 'Inter', sans-serif;
display: flex; align-items: center; justify-content: center; gap: 10px;
transition: all 0.3s;
box-shadow: 0 10px 20px var(--primary-glow);
position: relative; overflow: hidden;
}
.btn-connect:hover { transform: translateY(-3px); box-shadow: 0 15px 25px var(--primary-glow); }
.btn-connect:active { transform: translateY(0); box-shadow: 0 5px 10px var(--primary-glow); }
.btn-connect:disabled { opacity: 0.7; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-connect .spinner { display: none; font-size: 18px; animation: spin 1s linear infinite; }
@keyframes spin { 100% { transform: rotate(360deg); } }
/* ── Step Progress Bar ── */
.steps {
display: flex; gap: 4px; margin-bottom: 24px;
}
.steps .step {
flex: 1; height: 4px; border-radius: 2px;
background: var(--border); transition: background 0.5s;
}
.steps .step.done { background: #10b981; }
.steps .step.active { background: var(--primary); animation: stepPulse 1.2s infinite; }
@keyframes stepPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.status-msg { margin-top: 20px; font-size: 13px; font-weight: 600; min-height: 40px; color: var(--muted); line-height: 1.5; }
.status-msg.error { color: #ef4444; }
.status-msg.success { color: #10b981; }
/* Security badge */
.sec-badge {
display: inline-flex; align-items: center; gap: 6px;
margin-top: 30px; padding: 8px 16px; border-radius: 20px;
background: #f1f5f9; font-size: 11px; font-weight: 600; color: #64748b;
}
.sec-badge i { color: #10b981; font-size: 14px; }
</style>
</head>
<body>
<div class="portal-card">
<!-- Step progress indicator -->
<div class="steps" id="steps">
<div class="step" id="step1"></div>
<div class="step" id="step2"></div>
<div class="step" id="step3"></div>
<div class="step" id="step4"></div>
</div>
<div class="icon-wrap pulsing" id="iconWrap">
<i class="mdi mdi-bluetooth" id="btIcon"></i>
</div>
<h1>Identia Bluetooth</h1>
<p class="desc">Connect this device to the Bluetooth door module to proceed with the verification process.</p>
<button class="btn-connect" id="btnConnect">
<i class="mdi mdi-loading spinner" id="spinIcon"></i>
<i class="mdi mdi-bluetooth-connect" id="btnIcon"></i>
<span id="btnText">Connect Bluetooth</span>
</button>
<div class="status-msg" id="statusMsg"></div>
<div class="sec-badge">
<i class="mdi mdi-shield-check"></i> End-to-End Encryption
</div>
</div>
<script>
document.getElementById('btnConnect').addEventListener('click', async () => {
const btn = document.getElementById('btnConnect');
const btnText = document.getElementById('btnText');
const spinIcon = document.getElementById('spinIcon');
const btnIcon = document.getElementById('btnIcon');
const statusMsg = document.getElementById('statusMsg');
const iconWrap = document.getElementById('iconWrap');
const btIcon = document.getElementById('btIcon');
// Reset status
statusMsg.className = 'status-msg';
statusMsg.textContent = '';
try {
if (!navigator.bluetooth) {
throw new Error("Web Bluetooth is not supported in this browser. Please use Chrome and enable the feature if necessary.");
}
// ══════════════════════════════════════════
// STEP 1: Wake up ESP32 Bluetooth
// ══════════════════════════════════════════
btn.disabled = true;
btnIcon.style.display = 'none';
spinIcon.style.display = 'inline-block';
setStep(1);
btnText.textContent = "Waking up device...";
statusMsg.textContent = "Sending Bluetooth activation command to the door module...";
// Send the command to turn ON Bluetooth on ESP32
await fetch('api_wifi.php?action=trigger_bt_on');
// ══════════════════════════════════════════
// STEP 2: Wait for ESP32 to confirm BT is active
// Poll the server until bt_status == 'active'
// Timeout after 30 seconds (ESP32 polls every ~800ms)
// ══════════════════════════════════════════
setStep(2);
btnText.textContent = "Waiting for device...";
statusMsg.textContent = "Waiting for the door module to activate Bluetooth. Please wait...";
const btReady = await waitForBtActive(30000); // 30 second max wait
if (!btReady) {
throw new Error("The door module did not respond in time. Make sure the ESP32 is powered on and connected to the network.");
}
statusMsg.className = 'status-msg success';
statusMsg.textContent = "Door module is broadcasting! Connecting...";
// ══════════════════════════════════════════
// STEP 3: Connect to BLE device
// The browser picker will show identia_bt since
// ESP32 is now actively advertising
// ══════════════════════════════════════════
setStep(3);
btnText.textContent = "Connecting...";
statusMsg.textContent = "A device picker will appear. Please select 'identia_bt' and click Pair.";
const device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'identia_bt' }],
optionalServices: ['12345678-1234-5678-1234-56789abcdef0']
});
statusMsg.textContent = `Establishing connection to ${device.name}...`;
const server = await device.gatt.connect();
// Connected!
iconWrap.className = 'icon-wrap pulsing active';
btIcon.className = "mdi mdi-bluetooth-transfer";
statusMsg.className = 'status-msg success';
statusMsg.textContent = "Connected successfully! Generating secure access token...";
// ══════════════════════════════════════════
// STEP 4: Generate token and redirect
// ══════════════════════════════════════════
setStep(4);
btnText.textContent = "Preparing access...";
const resp = await fetch('api_generate_door_token.php');
const data = await resp.json();
if (data.status === 'success') {
statusMsg.textContent = "Redirecting to verification portal...";
btnText.textContent = "Redirecting...";
// All 4 steps done
document.querySelectorAll('.steps .step').forEach(s => s.className = 'step done');
setTimeout(() => {
window.location.href = `192.168.1.20.php?token=${data.token}`;
}, 600);
} else {
throw new Error(data.message || "Failed to generate access token.");
}
} catch(err) {
// Return to normal UI
btn.disabled = false;
btnText.textContent = "Connect Bluetooth";
btnIcon.style.display = 'inline-block';
spinIcon.style.display = 'none';
iconWrap.className = 'icon-wrap pulsing error';
btIcon.className = "mdi mdi-bluetooth-off";
resetSteps();
statusMsg.className = 'status-msg error';
if (err.name === 'NotFoundError') {
statusMsg.textContent = 'Cancelled or device not found. Please try again.';
} else if (err.name === 'SecurityError') {
statusMsg.textContent = 'Permission denied. Ensure HTTPS is enabled or add this site to chrome://flags.';
} else {
statusMsg.textContent = err.message || 'Connection failed.';
}
// Reset icon after 3 seconds
setTimeout(() => {
iconWrap.className = 'icon-wrap pulsing';
btIcon.className = "mdi mdi-bluetooth";
}, 3000);
}
});
/**
* Poll the server until the ESP32 confirms Bluetooth is active.
* @param {number} timeoutMs Maximum time to wait in milliseconds
* @returns {Promise<boolean>} true if BT became active, false if timed out
*/
async function waitForBtActive(timeoutMs) {
const startTime = Date.now();
const pollInterval = 1000; // Check every 1 second
while (Date.now() - startTime < timeoutMs) {
try {
const res = await fetch('api_wifi.php?action=get_bt_status');
const data = await res.json();
if (data.bt_status === 'active') {
// ESP32 has confirmed BT is on and advertising
// Wait a tiny bit more for the BLE stack to stabilize
await sleep(500);
return true;
}
// Update the status message with a visual countdown
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const remaining = Math.ceil((timeoutMs - (Date.now() - startTime)) / 1000);
document.getElementById('statusMsg').textContent =
`Waiting for the door module to activate Bluetooth... (${remaining}s remaining)`;
} catch(e) {
console.error('Poll error:', e);
}
await sleep(pollInterval);
}
return false; // Timed out
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function setStep(num) {
for (let i = 1; i <= 4; i++) {
const el = document.getElementById('step' + i);
if (i < num) el.className = 'step done';
else if (i === num) el.className = 'step active';
else el.className = 'step';
}
}
function resetSteps() {
for (let i = 1; i <= 4; i++) {
document.getElementById('step' + i).className = 'step';
}
}
</script>
</body>
</html>