Modification site web

This commit is contained in:
Guillaume-Sanchez
2026-06-29 23:44:32 +02:00
parent 27f53835c2
commit ae80490684
13 changed files with 1357 additions and 750 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
+447 -718
View File
File diff suppressed because it is too large Load Diff
+18 -13
View File
@@ -1,17 +1,22 @@
{ {
"name": "SafeAccess — Gestion de badges", "name": "CyberusGate Access",
"short_name": "SafeAccess", "short_name": "CG Access",
"description": "Gestion de badges d'accès NFC, RFID, QR Code et Bluetooth avec 2FA", "start_url": "index.html",
"start_url": "/index.html",
"display": "standalone", "display": "standalone",
"background_color": "#0D2B5E", "background_color": "#070c16",
"theme_color": "#1565C0", "theme_color": "#070c16",
"orientation": "portrait",
"icons": [ "icons": [
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, {
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } "src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDBkMmZmIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiPjwvcGF0aD48L3N2Zz4=",
], "sizes": "192x192",
"categories": ["security", "utilities", "business"], "type": "image/svg+xml",
"lang": "fr", "purpose": "any maskable"
"scope": "/" },
{
"src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDBkMmZmIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiPjwvcGF0aD48L3N2Zz4=",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
} }
+6 -8
View File
@@ -1,12 +1,10 @@
const CACHE = 'safeaccess-v1'; const CACHE_NAME = 'cyberus-cache-v3';
const ASSETS = ['/', '/index.html', '/manifest.json']; const urlsToCache = ['./', './index.html', './manifest.json'];
self.addEventListener('install', e => { self.addEventListener('install', event => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS))); event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache)));
}); });
self.addEventListener('fetch', e => { self.addEventListener('fetch', event => {
e.respondWith( event.respondWith(caches.match(event.request).then(response => response || fetch(event.request)));
caches.match(e.request).then(r => r || fetch(e.request)).catch(() => caches.match('/index.html'))
);
}); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

+844
View File
@@ -0,0 +1,844 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1565C0">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="SafeAccess">
<title>SafeAccess</title>
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<style>
:root {
--blue-dark: #0D2B5E;
--blue-main: #1565C0;
--blue-light: #1E88E5;
--blue-pale: #E3F2FD;
--green: #2E7D32;
--green-light: #E8F5E9;
--red: #C62828;
--red-light: #FFEBEE;
--orange: #E65100;
--gray-1: #F5F6FA;
--gray-2: #E8ECF1;
--gray-3: #B0BEC5;
--gray-4: #607D8B;
--gray-5: #37474F;
--white: #FFFFFF;
--text-main: #1A2332;
--text-sub: #546E7A;
--radius: 14px;
--radius-sm: 8px;
--shadow: 0 2px 16px rgba(0,0,0,0.10);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.16);
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--gray-1); color: var(--text-main); min-height: 100vh; overflow-x: hidden; }
/* SCREENS */
.screen { display: none; min-height: 100vh; flex-direction: column; }
.screen.active { display: flex; }
/* ======= SPLASH / LOGIN ======= */
#screen-login { background: linear-gradient(145deg, #0D2B5E 0%, #1565C0 60%, #1E88E5 100%); align-items: center; justify-content: center; padding: 40px 24px; }
.login-logo { width: 72px; height: 72px; background: rgba(255,255,255,0.15); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; border: 2px solid rgba(255,255,255,0.3); }
.login-logo svg { width: 40px; height: 40px; fill: white; }
.login-title { font-size: 28px; font-weight: 700; color: white; letter-spacing: -0.5px; margin-bottom: 4px; }
.login-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin-bottom: 40px; }
.login-card { background: white; border-radius: 20px; padding: 28px 24px; width: 100%; max-width: 380px; box-shadow: var(--shadow-lg); }
.login-card h2 { font-size: 18px; font-weight: 600; color: var(--text-main); margin-bottom: 4px; }
.login-card p { font-size: 13px; color: var(--text-sub); margin-bottom: 24px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 12px; font-weight: 600; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.form-group input { width: 100%; padding: 12px 14px; border: 1.5px solid var(--gray-2); border-radius: var(--radius-sm); font-size: 15px; color: var(--text-main); background: var(--gray-1); outline: none; transition: border-color 0.2s; }
.form-group input:focus { border-color: var(--blue-main); background: white; }
.btn-primary { width: 100%; padding: 14px; background: var(--blue-main); color: white; border: none; border-radius: var(--radius-sm); font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; }
.btn-primary:active { background: var(--blue-dark); transform: scale(0.98); }
.btn-outline { width: 100%; padding: 12px; background: transparent; color: var(--blue-main); border: 1.5px solid var(--blue-main); border-radius: var(--radius-sm); font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 10px; transition: all 0.2s; }
.btn-outline:active { background: var(--blue-pale); }
/* ======= 2FA ======= */
#screen-2fa { background: var(--gray-1); align-items: center; justify-content: center; padding: 40px 24px; }
.mfa-card { background: white; border-radius: 20px; padding: 32px 24px; width: 100%; max-width: 380px; box-shadow: var(--shadow-lg); text-align: center; }
.mfa-icon { width: 64px; height: 64px; border-radius: 50%; background: var(--blue-pale); display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; }
.mfa-icon svg { width: 32px; height: 32px; }
.mfa-card h2 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.mfa-card p { font-size: 14px; color: var(--text-sub); margin-bottom: 8px; }
.mfa-code-display { font-size: 36px; font-weight: 800; letter-spacing: 8px; color: var(--blue-main); margin: 20px 0; font-variant-numeric: tabular-nums; }
.mfa-progress { height: 4px; background: var(--gray-2); border-radius: 2px; overflow: hidden; margin-bottom: 20px; }
.mfa-progress-bar { height: 100%; background: var(--blue-main); border-radius: 2px; transition: width 1s linear; }
.mfa-input-row { display: flex; gap: 8px; justify-content: center; margin-bottom: 20px; }
.mfa-digit { width: 44px; height: 52px; border: 2px solid var(--gray-2); border-radius: 10px; font-size: 22px; font-weight: 700; text-align: center; color: var(--text-main); outline: none; transition: border-color 0.2s; }
.mfa-digit:focus { border-color: var(--blue-main); }
.mfa-digit.filled { border-color: var(--blue-light); background: var(--blue-pale); }
.mfa-method-row { display: flex; gap: 8px; margin-top: 12px; }
.mfa-method-btn { flex: 1; padding: 10px 8px; border: 1.5px solid var(--gray-2); border-radius: var(--radius-sm); font-size: 12px; font-weight: 600; color: var(--text-sub); background: white; cursor: pointer; transition: all 0.2s; }
.mfa-method-btn.active { border-color: var(--blue-main); color: var(--blue-main); background: var(--blue-pale); }
/* ======= MAIN APP ======= */
#screen-app { background: var(--gray-1); }
.app-header { background: var(--blue-dark); padding: 48px 20px 20px; display: flex; align-items: center; justify-content: space-between; }
.app-header-left { display: flex; align-items: center; gap: 12px; }
.app-header-avatar { width: 40px; height: 40px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.3); overflow: hidden; background: var(--blue-light); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: white; }
.app-header-info h1 { font-size: 16px; font-weight: 700; color: white; }
.app-header-info p { font-size: 12px; color: rgba(255,255,255,0.65); }
.app-header-right { display: flex; gap: 10px; }
.icon-btn { width: 38px; height: 38px; border-radius: 50%; background: rgba(255,255,255,0.12); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.icon-btn svg { width: 20px; height: 20px; stroke: white; fill: none; stroke-width: 2; }
/* NAV TABS */
.nav-tabs { display: flex; background: white; border-bottom: 1px solid var(--gray-2); position: sticky; top: 0; z-index: 100; }
.nav-tab { flex: 1; padding: 12px 4px; display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 10px; font-weight: 600; color: var(--gray-3); text-transform: uppercase; letter-spacing: 0.3px; border: none; background: transparent; cursor: pointer; position: relative; transition: color 0.2s; }
.nav-tab.active { color: var(--blue-main); }
.nav-tab.active::after { content: ''; position: absolute; bottom: 0; left: 20%; right: 20%; height: 2px; background: var(--blue-main); border-radius: 2px 2px 0 0; }
.nav-tab svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 2; }
/* TAB CONTENT */
.tab-pane { display: none; padding: 16px; flex-direction: column; gap: 14px; }
.tab-pane.active { display: flex; }
/* BADGE CARD */
.badge-card { background: white; border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.badge-card-header { background: linear-gradient(135deg, var(--blue-dark), var(--blue-main)); padding: 20px; display: flex; align-items: center; gap: 14px; }
.badge-avatar { width: 56px; height: 56px; border-radius: 50%; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 700; color: white; flex-shrink: 0; }
.badge-info h2 { font-size: 18px; font-weight: 700; color: white; }
.badge-info p { font-size: 13px; color: rgba(255,255,255,0.7); }
.badge-id { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 2px; }
.badge-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 700; margin-top: 14px; }
.badge-status.ready { background: rgba(46,125,50,0.2); color: #A5D6A7; border: 1px solid rgba(165,214,167,0.3); }
.badge-status.offline { background: rgba(198,40,40,0.2); color: #EF9A9A; border: 1px solid rgba(239,154,154,0.3); }
.badge-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #69F0AE; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
/* QR ZONE */
.qr-zone { padding: 20px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
.qr-box { width: 170px; height: 170px; border: 3px solid var(--blue-main); border-radius: 12px; padding: 8px; position: relative; }
.qr-corner { position: absolute; width: 18px; height: 18px; border-color: var(--blue-dark); border-style: solid; }
.qr-corner.tl { top: -2px; left: -2px; border-width: 3px 0 0 3px; border-radius: 4px 0 0 0; }
.qr-corner.tr { top: -2px; right: -2px; border-width: 3px 3px 0 0; border-radius: 0 4px 0 0; }
.qr-corner.bl { bottom: -2px; left: -2px; border-width: 0 0 3px 3px; border-radius: 0 0 0 4px; }
.qr-corner.br { bottom: -2px; right: -2px; border-width: 0 3px 3px 0; border-radius: 0 0 4px 0; }
.qr-canvas { width: 100%; height: 100%; }
.badge-system-tag { font-size: 11px; color: var(--gray-4); font-weight: 500; }
.badge-actions { padding: 0 16px 16px; display: flex; flex-direction: column; gap: 10px; }
.btn-action { padding: 13px; border-radius: var(--radius-sm); font-size: 15px; font-weight: 600; cursor: pointer; border: none; transition: all 0.15s; text-align: center; }
.btn-blue { background: var(--blue-main); color: white; }
.btn-blue:active { background: var(--blue-dark); transform: scale(0.97); }
.btn-ghost { background: transparent; color: var(--blue-main); border: 1.5px solid var(--blue-main); }
.btn-ghost:active { background: var(--blue-pale); }
/* NFC ZONE */
.nfc-area { padding: 20px; display: flex; flex-direction: column; align-items: center; }
.nfc-ring { width: 120px; height: 120px; border-radius: 50%; border: 3px solid var(--blue-light); display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 14px; animation: nfc-pulse 2.5s ease-in-out infinite; }
@keyframes nfc-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(21,101,192,0.3);} 50%{box-shadow:0 0 0 18px rgba(21,101,192,0);} }
.nfc-ring svg { width: 52px; height: 52px; stroke: var(--blue-main); fill: none; stroke-width: 1.5; }
.nfc-status { font-size: 14px; font-weight: 600; color: var(--blue-main); }
.nfc-sub { font-size: 12px; color: var(--text-sub); text-align: center; margin-top: 4px; }
/* BADGES LIST */
.section-title { font-size: 13px; font-weight: 700; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 2px; }
.badge-list-item { display: flex; align-items: center; gap: 12px; padding: 14px; background: white; border-radius: var(--radius); box-shadow: var(--shadow); cursor: pointer; transition: transform 0.15s; }
.badge-list-item:active { transform: scale(0.98); }
.badge-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.badge-icon svg { width: 24px; height: 24px; }
.badge-icon.nfc { background: #E3F2FD; }
.badge-icon.nfc svg { stroke: var(--blue-main); fill: none; stroke-width: 2; }
.badge-icon.rfid { background: #F3E5F5; }
.badge-icon.rfid svg { stroke: #7B1FA2; fill: none; stroke-width: 2; }
.badge-icon.qr { background: #E8F5E9; }
.badge-icon.qr svg { stroke: var(--green); fill: none; stroke-width: 2; }
.badge-icon.bt { background: #FFF3E0; }
.badge-icon.bt svg { stroke: #E65100; fill: none; stroke-width: 2; }
.badge-list-info { flex: 1; min-width: 0; }
.badge-list-name { font-size: 15px; font-weight: 600; color: var(--text-main); }
.badge-list-sub { font-size: 12px; color: var(--text-sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.badge-list-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.tech-chip { font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 10px; text-transform: uppercase; }
.tech-chip.nfc { background: var(--blue-pale); color: var(--blue-main); }
.tech-chip.rfid { background: #F3E5F5; color: #7B1FA2; }
.tech-chip.qr { background: var(--green-light); color: var(--green); }
.tech-chip.bt { background: #FFF3E0; color: var(--orange); }
.active-dot { width: 8px; height: 8px; border-radius: 50%; }
.active-dot.on { background: #4CAF50; }
.active-dot.off { background: var(--gray-3); }
/* HISTORY */
.history-search { display: flex; align-items: center; gap: 10px; background: white; border-radius: var(--radius-sm); padding: 10px 14px; box-shadow: var(--shadow); }
.history-search svg { width: 18px; height: 18px; stroke: var(--gray-3); fill: none; stroke-width: 2; flex-shrink: 0; }
.history-search input { flex: 1; border: none; outline: none; font-size: 14px; color: var(--text-main); background: transparent; }
.history-group-label { font-size: 12px; font-weight: 700; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.5px; padding: 4px 0; }
.history-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; background: white; border-radius: var(--radius-sm); box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
.history-icon { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.history-icon.in { background: var(--green-light); }
.history-icon.in svg { stroke: var(--green); }
.history-icon.out { background: #E3F2FD; }
.history-icon.out svg { stroke: var(--blue-main); }
.history-icon.refused { background: var(--red-light); }
.history-icon.refused svg { stroke: var(--red); }
.history-icon svg { width: 18px; height: 18px; fill: none; stroke-width: 2.5; }
.history-info { flex: 1; }
.history-info .loc { font-size: 14px; font-weight: 600; color: var(--text-main); }
.history-info .type { font-size: 12px; color: var(--text-sub); }
.history-right { text-align: right; }
.history-right .status { font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 10px; }
.status.ok { background: var(--green-light); color: var(--green); }
.status.refused { background: var(--red-light); color: var(--red); }
.history-right .time { font-size: 11px; color: var(--gray-4); margin-top: 3px; }
/* DASHBOARD */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.stat-card { background: white; border-radius: var(--radius); padding: 16px; box-shadow: var(--shadow); }
.stat-card-label { font-size: 11px; font-weight: 600; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.stat-card-value { font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1; }
.stat-card-sub { font-size: 12px; color: var(--text-sub); margin-top: 4px; }
.stat-card.blue .stat-card-value { color: var(--blue-main); }
.stat-card.green .stat-card-value { color: var(--green); }
.stat-card.red .stat-card-value { color: var(--red); }
.stat-card.orange .stat-card-value { color: var(--orange); }
.zone-list-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; background: white; border-radius: var(--radius-sm); box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
.zone-info .zone-name { font-size: 14px; font-weight: 600; }
.zone-info .zone-level { font-size: 12px; color: var(--text-sub); }
.zone-badge { font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 10px; }
.zone-badge.high { background: var(--red-light); color: var(--red); }
.zone-badge.medium { background: #FFF3E0; color: var(--orange); }
.zone-badge.low { background: var(--green-light); color: var(--green); }
/* SETTINGS */
.settings-section { background: white; border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); }
.settings-item { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--gray-2); cursor: pointer; transition: background 0.15s; }
.settings-item:last-child { border-bottom: none; }
.settings-item:active { background: var(--gray-1); }
.settings-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.settings-icon svg { width: 20px; height: 20px; }
.settings-icon.blue { background: var(--blue-pale); }
.settings-icon.blue svg { stroke: var(--blue-main); fill: none; stroke-width: 2; }
.settings-icon.green { background: var(--green-light); }
.settings-icon.green svg { stroke: var(--green); fill: none; stroke-width: 2; }
.settings-icon.red { background: var(--red-light); }
.settings-icon.red svg { stroke: var(--red); fill: none; stroke-width: 2; }
.settings-icon.orange { background: #FFF3E0; }
.settings-icon.orange svg { stroke: var(--orange); fill: none; stroke-width: 2; }
.settings-text { flex: 1; }
.settings-text .s-title { font-size: 15px; font-weight: 600; }
.settings-text .s-sub { font-size: 12px; color: var(--text-sub); }
.settings-arrow { color: var(--gray-3); }
.toggle { width: 44px; height: 24px; background: var(--gray-3); border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
.toggle.on { background: var(--blue-main); }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: white; transition: left 0.2s; box-shadow: 0 1px 4px rgba(0,0,0,0.2); }
.toggle.on::after { left: 22px; }
/* NOTIFICATIONS */
.notif-banner { background: var(--blue-pale); border-left: 3px solid var(--blue-main); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding: 12px 14px; display: flex; gap: 10px; align-items: flex-start; }
.notif-banner.warning { background: #FFF8E1; border-color: #FBC02D; }
.notif-banner.success { background: var(--green-light); border-color: var(--green); }
.notif-icon { width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px; }
.notif-icon svg { width: 20px; height: 20px; fill: none; stroke-width: 2; }
.notif-banner .n-title { font-size: 13px; font-weight: 700; color: var(--text-main); }
.notif-banner .n-sub { font-size: 12px; color: var(--text-sub); }
/* MODAL */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: flex-end; }
.modal-overlay.open { display: flex; }
.modal-sheet { background: white; border-radius: 20px 20px 0 0; padding: 8px 20px 32px; width: 100%; max-height: 80vh; overflow-y: auto; }
.modal-handle { width: 36px; height: 4px; background: var(--gray-2); border-radius: 2px; margin: 10px auto 20px; }
.modal-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; }
.add-badge-option { display: flex; align-items: center; gap: 14px; padding: 14px; border-radius: var(--radius-sm); border: 1.5px solid var(--gray-2); margin-bottom: 10px; cursor: pointer; transition: all 0.15s; }
.add-badge-option:active { background: var(--gray-1); border-color: var(--blue-main); }
.add-badge-opt-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.add-badge-opt-icon svg { width: 24px; height: 24px; fill: none; stroke-width: 2; }
.add-badge-opt-text .t { font-size: 15px; font-weight: 600; }
.add-badge-opt-text .s { font-size: 12px; color: var(--text-sub); }
/* TOAST */
.toast { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%) translateY(20px); background: #263238; color: white; padding: 10px 20px; border-radius: 24px; font-size: 14px; font-weight: 500; opacity: 0; transition: all 0.3s; z-index: 9999; white-space: nowrap; pointer-events: none; }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* FAB */
.fab { position: fixed; bottom: 80px; right: 20px; width: 52px; height: 52px; border-radius: 50%; background: var(--blue-main); border: none; box-shadow: 0 4px 16px rgba(21,101,192,0.4); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 200; transition: transform 0.15s; }
.fab:active { transform: scale(0.9); }
.fab svg { width: 26px; height: 26px; stroke: white; fill: none; stroke-width: 2; }
/* CHIP ROW */
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
.chip-row::-webkit-scrollbar { display: none; }
.chip { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 600; cursor: pointer; border: 1.5px solid var(--gray-2); color: var(--text-sub); background: white; white-space: nowrap; transition: all 0.15s; }
.chip.active { background: var(--blue-main); color: white; border-color: var(--blue-main); }
</style>
</head>
<body>
<!-- ===== LOGIN ===== -->
<div id="screen-login" class="screen active">
<div style="text-align:center;">
<div class="login-logo" style="margin:0 auto 16px;">
<svg viewBox="0 0 40 40"><path d="M20 4L6 12v16l14 8 14-8V12L20 4z" fill="white" opacity="0.9"/><circle cx="20" cy="20" r="6" fill="#1E88E5"/></svg>
</div>
<div class="login-title">SafeAccess</div>
<div class="login-sub">Gestion de badges d'accès sécurisé</div>
</div>
<div class="login-card">
<h2>Connexion</h2>
<p>Entrez vos identifiants pour continuer</p>
<div class="form-group">
<label>Identifiant</label>
<input type="email" id="login-email" placeholder="alice.dupont@société.fr" value="alice.dupont@societe.fr">
</div>
<div class="form-group">
<label>Mot de passe</label>
<input type="password" id="login-pass" placeholder="••••••••" value="password123">
</div>
<button class="btn-primary" onclick="goTo2FA()">Se connecter →</button>
<button class="btn-outline" onclick="showToast('Contactez votre administrateur')">Mot de passe oublié</button>
</div>
</div>
<!-- ===== 2FA ===== -->
<div id="screen-2fa" class="screen">
<div class="mfa-card">
<div class="mfa-icon">
<svg viewBox="0 0 32 32" fill="none" stroke="#1565C0" stroke-width="2"><rect x="8" y="14" width="16" height="12" rx="3"/><path d="M11 14v-4a5 5 0 0110 0v4"/><circle cx="16" cy="21" r="2" fill="#1565C0" stroke="none"/></svg>
</div>
<h2>Vérification 2FA</h2>
<p>Code TOTP (renouvelle toutes les 30s)</p>
<div class="mfa-code-display" id="totp-display">------</div>
<div class="mfa-progress"><div class="mfa-progress-bar" id="totp-bar" style="width:100%"></div></div>
<p style="font-size:13px;color:var(--text-sub);margin-bottom:12px;">Entrez le code de votre application d'authentification :</p>
<div class="mfa-input-row" id="mfa-inputs"></div>
<div class="mfa-method-row">
<button class="mfa-method-btn active" onclick="selectMFA(this,'totp')">📱 TOTP</button>
<button class="mfa-method-btn" onclick="selectMFA(this,'sms')">💬 SMS</button>
<button class="mfa-method-btn" onclick="selectMFA(this,'email')">📧 Email</button>
</div>
<button class="btn-primary" style="margin-top:20px;" onclick="verifyMFA()">Vérifier →</button>
<button class="btn-outline" style="margin-top:8px;" onclick="goTo('screen-login')">← Retour</button>
</div>
</div>
<!-- ===== MAIN APP ===== -->
<div id="screen-app" class="screen">
<div class="app-header">
<div class="app-header-left">
<div class="app-header-avatar">AD</div>
<div class="app-header-info">
<h1>Alice Dupont</h1>
<p>AC-0012 · Niveau 3</p>
</div>
</div>
<div class="app-header-right">
<button class="icon-btn" onclick="showToast('Notifications')">
<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</button>
<button class="icon-btn" onclick="showToast('Paramètres rapides')">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</button>
</div>
</div>
<nav class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('badge',this)">
<svg viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="3"/><path d="M8 12h.01M12 12h.01M16 12h.01"/></svg>
Badge
</button>
<button class="nav-tab" onclick="switchTab('mes-badges',this)">
<svg viewBox="0 0 24 24"><path d="M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2z"/><path d="M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"/></svg>
Mes badges
</button>
<button class="nav-tab" onclick="switchTab('historique',this)">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Historique
</button>
<button class="nav-tab" onclick="switchTab('dashboard',this)">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Tableau
</button>
<button class="nav-tab" onclick="switchTab('settings',this)">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Réglages
</button>
</nav>
<!-- TAB: MON E-BADGE -->
<div id="tab-badge" class="tab-pane active">
<div class="notif-banner success">
<div class="notif-icon"><svg viewBox="0 0 20 20" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M9 12l2 2 4-4"/><path d="M4 6a8 8 0 1016 0A8 8 0 004 6"/></svg></div>
<div><div class="n-title">NFC disponible</div><div class="n-sub">Votre téléphone est détecté compatible NFC</div></div>
</div>
<div class="badge-card">
<div class="badge-card-header">
<div class="badge-avatar">AD</div>
<div class="badge-info">
<h2>Alice Dupont</h2>
<p>Technicien Réseau Senior</p>
<div class="badge-id">ID: AC-0012 · Émis le 01/01/2025</div>
<div class="badge-status ready">
<div class="badge-status-dot"></div>
PRÊT À SCANNER
</div>
</div>
</div>
<div class="qr-zone">
<div class="qr-box">
<canvas id="qr-canvas" class="qr-canvas" width="154" height="154"></canvas>
<div class="qr-corner tl"></div><div class="qr-corner tr"></div>
<div class="qr-corner bl"></div><div class="qr-corner br"></div>
</div>
<div class="badge-system-tag">SafeAccess system · NFC/QR</div>
</div>
<div class="nfc-area">
<div class="nfc-ring" id="nfc-ring">
<svg viewBox="0 0 52 52"><path d="M26 8C16 8 8 16 8 26s8 18 18 18 18-8 18-18"/><path d="M26 14c-7 0-12 5-12 12s5 12 12 12 12-5 12-12"/><path d="M26 20c-3 0-6 3-6 6s3 6 6 6 6-3 6-6"/><circle cx="26" cy="26" r="3" fill="#1565C0"/></svg>
</div>
<div class="nfc-status" id="nfc-status-text">En attente d'un lecteur NFC</div>
<div class="nfc-sub">Approchez votre téléphone du lecteur</div>
</div>
<div class="badge-actions">
<button class="btn-action btn-blue" onclick="simulateNFC()">📡 SIMULER LECTURE NFC</button>
<button class="btn-action btn-ghost" onclick="switchTab('historique', document.querySelectorAll('.nav-tab')[2])">HISTORIQUE D'ACCÈS</button>
</div>
</div>
</div>
<!-- TAB: MES BADGES -->
<div id="tab-mes-badges" class="tab-pane">
<div class="chip-row">
<div class="chip active">Tous</div>
<div class="chip">NFC</div>
<div class="chip">RFID</div>
<div class="chip">QR Code</div>
<div class="chip">Bluetooth</div>
</div>
<div class="section-title">Mes badges (4)</div>
<div id="badge-list"></div>
</div>
<!-- TAB: HISTORIQUE -->
<div id="tab-historique" class="tab-pane">
<div class="history-search">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input type="text" placeholder="Rechercher un accès..." id="history-search">
</div>
<div id="history-list"></div>
</div>
<!-- TAB: DASHBOARD -->
<div id="tab-dashboard" class="tab-pane">
<div class="section-title">Tableau de bord</div>
<div class="stat-grid">
<div class="stat-card blue"><div class="stat-card-label">Utilisateurs</div><div class="stat-card-value">17</div><div class="stat-card-sub">actifs ce mois</div></div>
<div class="stat-card"><div class="stat-card-label">Zones</div><div class="stat-card-value">16</div><div class="stat-card-sub">zones actives</div></div>
<div class="stat-card green"><div class="stat-card-label">Accès aujourd'hui</div><div class="stat-card-value">143</div><div class="stat-card-sub">autorisés</div></div>
<div class="stat-card red"><div class="stat-card-label">Alertes</div><div class="stat-card-value">0</div><div class="stat-card-sub">incidents actifs</div></div>
</div>
<div class="section-title" style="margin-top:4px;">Gestion des zones</div>
<div id="zone-list"></div>
<div class="section-title" style="margin-top:4px;">Logs récents</div>
<div id="dash-logs"></div>
</div>
<!-- TAB: RÉGLAGES -->
<div id="tab-settings" class="tab-pane">
<div class="section-title">Sécurité</div>
<div class="settings-section">
<div class="settings-item">
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 018 0v4"/><circle cx="12" cy="16" r="1" fill="#1565C0"/></svg></div>
<div class="settings-text"><div class="s-title">Double authentification (2FA)</div><div class="s-sub">TOTP activé · App Authenticator</div></div>
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-item">
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
<div class="settings-text"><div class="s-title">Biométrie</div><div class="s-sub">Empreinte / Face ID</div></div>
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-item" onclick="showToast('PIN modifié')">
<div class="settings-icon orange"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg></div>
<div class="settings-text"><div class="s-title">Code PIN</div><div class="s-sub">Modifier le code PIN de secours</div></div>
<div class="settings-arrow"></div>
</div>
</div>
<div class="section-title" style="margin-top:4px;">NFC & Technologies</div>
<div class="settings-section">
<div class="settings-item">
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><path d="M12 7c-2.5 0-5 2.5-5 5s2.5 5 5 5 5-2.5 5-5"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg></div>
<div class="settings-text"><div class="s-title">NFC actif</div><div class="s-sub">Lecture/écriture NFC activée</div></div>
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-item">
<div class="settings-icon orange"><svg viewBox="0 0 24 24"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/></svg></div>
<div class="settings-text"><div class="s-title">Bluetooth BLE</div><div class="s-sub">Badges longue portée</div></div>
<div class="toggle" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-item">
<div class="settings-icon green"><svg viewBox="0 0 24 24"><path d="M3 3h18v18H3z"/><path d="M9 9h2v2H9zM13 9h2v2h-2zM9 13h2v2H9z"/></svg></div>
<div class="settings-text"><div class="s-title">QR Code dynamique</div><div class="s-sub">Régénération toutes les 60s</div></div>
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
</div>
</div>
<div class="section-title" style="margin-top:4px;">Compte</div>
<div class="settings-section">
<div class="settings-item" onclick="showToast('Profil ouvert')">
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
<div class="settings-text"><div class="s-title">Mon profil</div><div class="s-sub">Alice Dupont · AC-0012</div></div>
<div class="settings-arrow"></div>
</div>
<div class="settings-item" onclick="goTo('screen-login')">
<div class="settings-icon red"><svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></div>
<div class="settings-text"><div class="s-title">Déconnexion</div><div class="s-sub">alice.dupont@societe.fr</div></div>
<div class="settings-arrow"></div>
</div>
</div>
<div style="text-align:center;padding:20px 0;font-size:11px;color:var(--gray-3);">SafeAccess v2.4.1 · Build 2025.06</div>
</div>
</div>
<!-- FAB -->
<button class="fab" id="fab-btn" style="display:none;" onclick="showAddBadgeModal()">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<!-- MODAL -->
<div class="modal-overlay" id="modal-add">
<div class="modal-sheet">
<div class="modal-handle"></div>
<div class="modal-title">Ajouter un badge</div>
<div class="add-badge-option" onclick="addBadge('NFC')">
<div class="add-badge-opt-icon" style="background:#E3F2FD;"><svg viewBox="0 0 24 24" fill="none" stroke="#1565C0" stroke-width="2"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg></div>
<div class="add-badge-opt-text"><div class="t">Badge NFC</div><div class="s">ISO 14443 · Mifare · NFC Type A/B</div></div>
</div>
<div class="add-badge-option" onclick="addBadge('RFID')">
<div class="add-badge-opt-icon" style="background:#F3E5F5;"><svg viewBox="0 0 24 24" fill="none" stroke="#7B1FA2" stroke-width="2"><path d="M5 12.5c0-3.9 3.1-7 7-7s7 3.1 7 7"/><path d="M8 12.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="12" cy="12.5" r="2"/></svg></div>
<div class="add-badge-opt-text"><div class="t">Badge RFID</div><div class="s">125kHz · HID · Em-Marine · Wiegand</div></div>
</div>
<div class="add-badge-option" onclick="addBadge('QR Code')">
<div class="add-badge-opt-icon" style="background:#E8F5E9;"><svg viewBox="0 0 24 24" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM17 14h.01M14 14h.01M14 17h.01M17 17h4M17 20v1M21 14v3"/></svg></div>
<div class="add-badge-opt-text"><div class="t">QR Code dynamique</div><div class="s">Rotation toutes les 60s · HMAC signé</div></div>
</div>
<div class="add-badge-option" onclick="addBadge('Bluetooth')">
<div class="add-badge-opt-icon" style="background:#FFF3E0;"><svg viewBox="0 0 24 24" fill="none" stroke="#E65100" stroke-width="2"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/><polyline points="17.5 12 6.5 17.5"/></svg></div>
<div class="add-badge-opt-text"><div class="t">Badge Bluetooth BLE</div><div class="s">Longue portée · iBeacon · Eddystone</div></div>
</div>
<button class="btn-outline" onclick="closeModal()" style="margin-top:8px;">Annuler</button>
</div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
// ===== STATE =====
let currentTab = 'badge';
let mfaEntered = '';
let totpTimer = 30;
let totpInterval = null;
const correctCode = '472851';
const badges = [
{ id: 'B001', name: 'Badge Principal', tech: 'nfc', uid: 'A3:F2:1B:CC', zones: ['Bureau Principal', 'Parking', 'Cafétéria'], active: true },
{ id: 'B002', name: 'Badge Labo', tech: 'rfid', uid: '125K-88F2A1', zones: ['Labo A', 'Labo B'], active: true },
{ id: 'B003', name: 'QR Code', tech: 'qr', uid: 'QR-DYN-2025', zones: ['Bureau Principal'], active: true },
{ id: 'B004', name: 'Badge BLE', tech: 'bt', uid: 'BT:A4:CC:12:FF', zones: ['Parking'], active: false },
];
const history = [
{ date: "Aujourd'hui, 09:30", type: 'in', loc: 'Bureau Principal', sub: '· NFC · B001', status: 'ok' },
{ date: "Hier, 17:15", type: 'out', loc: 'Parking', sub: '· RFID · B002', status: 'ok' },
{ date: "Mercredi, 14:05", type: 'in', loc: 'Labo A', sub: '· RFID · B002', status: 'refused' },
{ date: "Mercredi, 08:50", type: 'in', loc: 'Bureau Principal', sub: '· NFC · B001', status: 'ok' },
{ date: "Mardi, 18:20", type: 'out', loc: 'Bureau Principal', sub: '· QR · B003', status: 'ok' },
{ date: "Mardi, 09:00", type: 'in', loc: 'Cafétéria', sub: '· NFC · B001', status: 'ok' },
];
const zones = [
{ name: 'Bureau Principal', level: 'Niveau 1', risk: 'low' },
{ name: 'Labo A', level: 'Niveau 3 — Restreint', risk: 'high' },
{ name: 'Parking', level: 'Niveau 1', risk: 'low' },
{ name: 'Salle Serveurs', level: 'Niveau 4 — Critique', risk: 'high' },
{ name: 'Cafétéria', level: 'Niveau 0 — Public', risk: 'low' },
{ name: 'Direction', level: 'Niveau 2', risk: 'medium' },
];
// ===== NAVIGATION =====
function goTo(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
document.getElementById('fab-btn').style.display = screenId === 'screen-app' ? 'flex' : 'none';
}
function switchTab(tab, btn) {
currentTab = tab;
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
if(btn) btn.classList.add('active');
if(tab === 'mes-badges') renderBadgeList();
if(tab === 'historique') renderHistory();
if(tab === 'dashboard') renderDashboard();
}
// ===== LOGIN / 2FA =====
function goTo2FA() {
const e = document.getElementById('login-email').value;
const p = document.getElementById('login-pass').value;
if(!e || !p) { showToast('Renseignez vos identifiants'); return; }
goTo('screen-2fa');
initTOTP();
buildMFAInputs();
}
function initTOTP() {
if(totpInterval) clearInterval(totpInterval);
totpTimer = 30;
updateTOTP();
totpInterval = setInterval(() => {
totpTimer--;
if(totpTimer <= 0) { totpTimer = 30; generateNewCode(); }
updateTOTP();
}, 1000);
}
let currentCode = correctCode;
function generateNewCode() {
currentCode = String(Math.floor(100000 + Math.random() * 900000));
document.getElementById('totp-display').textContent = currentCode;
}
function updateTOTP() {
document.getElementById('totp-display').textContent = currentCode;
document.getElementById('totp-bar').style.width = (totpTimer / 30 * 100) + '%';
}
function buildMFAInputs() {
const row = document.getElementById('mfa-inputs');
row.innerHTML = '';
mfaEntered = '';
for(let i = 0; i < 6; i++) {
const inp = document.createElement('input');
inp.className = 'mfa-digit';
inp.maxLength = 1;
inp.inputMode = 'numeric';
inp.pattern = '[0-9]';
inp.dataset.idx = i;
inp.addEventListener('input', e => {
if(/[0-9]/.test(e.target.value)) {
e.target.classList.add('filled');
if(i < 5) row.children[i+1].focus();
} else { e.target.value = ''; e.target.classList.remove('filled'); }
});
inp.addEventListener('keydown', e => { if(e.key === 'Backspace' && !inp.value && i > 0) row.children[i-1].focus(); });
row.appendChild(inp);
}
setTimeout(() => row.children[0].focus(), 100);
}
function selectMFA(btn, method) {
document.querySelectorAll('.mfa-method-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if(method === 'sms') showToast('SMS envoyé au +33 6 ** ** ** 42');
if(method === 'email') showToast('Email envoyé à a.dupont@****');
}
function verifyMFA() {
const inputs = document.querySelectorAll('.mfa-digit');
let code = '';
inputs.forEach(i => code += i.value);
if(code.length < 6) { showToast('Entrez les 6 chiffres'); return; }
if(code === currentCode || code === correctCode) {
if(totpInterval) clearInterval(totpInterval);
showToast('✓ Authentification réussie !');
setTimeout(() => {
goTo('screen-app');
renderQR();
renderBadgeList();
renderHistory();
renderDashboard();
}, 800);
} else {
showToast('❌ Code incorrect, réessayez');
buildMFAInputs();
}
}
// ===== QR CODE =====
function renderQR() {
const canvas = document.getElementById('qr-canvas');
const ctx = canvas.getContext('2d');
const size = 154;
const data = 'SAFEACCESS:AC-0012:' + Date.now();
drawQR(ctx, data, size);
setInterval(() => {
const newData = 'SAFEACCESS:AC-0012:' + Date.now();
drawQR(ctx, newData, size);
}, 60000);
}
function drawQR(ctx, data, size) {
ctx.clearRect(0, 0, size, size);
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, size, size);
const cells = 25;
const cellSize = Math.floor(size / cells);
const hash = Array.from(data).reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0);
for(let r = 0; r < cells; r++) {
for(let c = 0; c < cells; c++) {
const seed = (r * 31 + c * 17 + hash + r*c) % 100;
const shouldFill = seed < 45 || (r < 7 && c < 7) || (r < 7 && c > cells-8) || (r > cells-8 && c < 7);
if(shouldFill) {
ctx.fillStyle = '#0D2B5E';
ctx.fillRect(c * cellSize + 1, r * cellSize + 1, cellSize - 1, cellSize - 1);
}
}
}
// Finder patterns
[[0,0],[0,cells-7],[cells-7,0]].forEach(([row,col]) => {
ctx.strokeStyle = '#0D2B5E'; ctx.lineWidth = 2;
ctx.strokeRect(col*cellSize+1, row*cellSize+1, 6*cellSize, 6*cellSize);
ctx.fillStyle = '#0D2B5E';
ctx.fillRect((col+2)*cellSize, (row+2)*cellSize, 3*cellSize, 3*cellSize);
});
}
// ===== NFC SIMULATION =====
function simulateNFC() {
const ring = document.getElementById('nfc-ring');
const status = document.getElementById('nfc-status-text');
ring.style.borderColor = '#FBC02D';
ring.style.animation = 'nfc-pulse 0.8s ease-in-out infinite';
status.textContent = 'Lecture NFC en cours...';
status.style.color = '#E65100';
setTimeout(() => {
ring.style.borderColor = '#4CAF50';
ring.style.animation = 'none';
status.textContent = '✓ Accès autorisé — Bureau Principal';
status.style.color = '#2E7D32';
showToast('✓ Accès autorisé — Bureau Principal');
setTimeout(() => {
ring.style.borderColor = 'var(--blue-light)';
ring.style.animation = 'nfc-pulse 2.5s ease-in-out infinite';
status.textContent = 'En attente d\'un lecteur NFC';
status.style.color = 'var(--blue-main)';
}, 3000);
}, 1800);
}
// ===== BADGE LIST =====
const techIcons = {
nfc: `<svg viewBox="0 0 24 24" fill="none" stroke="#1565C0" stroke-width="2"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg>`,
rfid: `<svg viewBox="0 0 24 24" fill="none" stroke="#7B1FA2" stroke-width="2"><path d="M5 12.5c0-3.9 3.1-7 7-7s7 3.1 7 7"/><path d="M8 12.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="12" cy="12.5" r="2"/></svg>`,
qr: `<svg viewBox="0 0 24 24" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3z"/></svg>`,
bt: `<svg viewBox="0 0 24 24" fill="none" stroke="#E65100" stroke-width="2"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/></svg>`,
};
function renderBadgeList() {
const list = document.getElementById('badge-list');
list.innerHTML = badges.map(b => `
<div class="badge-list-item" onclick="showToast('Badge ${b.name} sélectionné')">
<div class="badge-icon ${b.tech}">${techIcons[b.tech]}</div>
<div class="badge-list-info">
<div class="badge-list-name">${b.name}</div>
<div class="badge-list-sub">${b.uid} · ${b.zones.join(', ')}</div>
</div>
<div class="badge-list-right">
<span class="tech-chip ${b.tech}">${b.tech.toUpperCase()}</span>
<div class="active-dot ${b.active ? 'on' : 'off'}"></div>
</div>
</div>
`).join('');
}
// ===== HISTORY =====
const inIcon = `<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>`;
const outIcon = `<svg viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>`;
const refIcon = `<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
function renderHistory(filter = '') {
const list = document.getElementById('history-list');
const filtered = history.filter(h => !filter || h.loc.toLowerCase().includes(filter.toLowerCase()));
let html = '';
let lastDate = '';
filtered.forEach(h => {
if(h.date !== lastDate) {
html += `<div class="history-group-label">${h.date}</div>`;
lastDate = h.date;
}
const iconType = h.status === 'refused' ? 'refused' : h.type === 'in' ? 'in' : 'out';
const icon = h.status === 'refused' ? refIcon : h.type === 'in' ? inIcon : outIcon;
const typeLabel = h.type === 'in' ? 'Entrée' : 'Sortie';
html += `
<div class="history-item">
<div class="history-icon ${iconType}">${icon}</div>
<div class="history-info">
<div class="loc">${typeLabel} · ${h.loc}</div>
<div class="type">${h.sub}</div>
</div>
<div class="history-right">
<span class="status ${h.status === 'ok' ? 'ok' : 'refused'}">${h.status === 'ok' ? 'AUTORISÉ' : 'REFUSÉ'}</span>
<div class="time">${h.date.split(',')[1] || ''}</div>
</div>
</div>`;
});
list.innerHTML = html || '<div style="text-align:center;padding:20px;color:var(--gray-4);">Aucun résultat</div>';
}
document.getElementById('history-search').addEventListener('input', e => renderHistory(e.target.value));
// ===== DASHBOARD =====
function renderDashboard() {
const zl = document.getElementById('zone-list');
zl.innerHTML = zones.map(z => `
<div class="zone-list-item">
<div class="zone-info"><div class="zone-name">${z.name}</div><div class="zone-level">${z.level}</div></div>
<span class="zone-badge ${z.risk}">${z.risk === 'high' ? 'Restreint' : z.risk === 'medium' ? 'Modéré' : 'Public'}</span>
</div>`).join('');
const dl = document.getElementById('dash-logs');
dl.innerHTML = history.slice(0,4).map(h => `
<div class="history-item" style="margin-bottom:8px;">
<div class="history-icon ${h.status === 'refused' ? 'refused' : h.type === 'in' ? 'in' : 'out'}">${h.status === 'refused' ? refIcon : h.type === 'in' ? inIcon : outIcon}</div>
<div class="history-info"><div class="loc">${h.loc}</div><div class="type">${h.sub}</div></div>
<div class="history-right"><span class="status ${h.status === 'ok' ? 'ok' : 'refused'}">${h.status === 'ok' ? 'OK' : 'REFUSÉ'}</span></div>
</div>`).join('');
}
// ===== ADD BADGE MODAL =====
function showAddBadgeModal() {
document.getElementById('modal-add').classList.add('open');
}
function closeModal() {
document.getElementById('modal-add').classList.remove('open');
}
function addBadge(tech) {
closeModal();
const newBadge = {
id: 'B00' + (badges.length + 1),
name: 'Nouveau badge ' + tech,
tech: tech.toLowerCase() === 'nfc' ? 'nfc' : tech.toLowerCase() === 'rfid' ? 'rfid' : tech.toLowerCase() === 'qr code' ? 'qr' : 'bt',
uid: 'NEW-' + Math.random().toString(36).substr(2,8).toUpperCase(),
zones: ['Bureau Principal'],
active: true,
};
badges.push(newBadge);
if(currentTab === 'mes-badges') renderBadgeList();
showToast('✓ Badge ' + tech + ' ajouté');
}
// ===== TOAST =====
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
}
// ===== INIT =====
document.getElementById('fab-btn').style.display = 'none';
</script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
{
"name": "SafeAccess — Gestion de badges",
"short_name": "SafeAccess",
"description": "Gestion de badges d'accès NFC, RFID, QR Code et Bluetooth avec 2FA",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#0D2B5E",
"theme_color": "#1565C0",
"orientation": "portrait",
"icons": [
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"categories": ["security", "utilities", "business"],
"lang": "fr",
"scope": "/"
}
+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+12
View File
@@ -0,0 +1,12 @@
const CACHE = 'safeaccess-v1';
const ASSETS = ['/', '/index.html', '/manifest.json'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
});
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(r => r || fetch(e.request)).catch(() => caches.match('/index.html'))
);
});