earwigmoth commited on
Commit
f0146d8
Β·
verified Β·
1 Parent(s): 3e64e35
Files changed (3) hide show
  1. admin.css +150 -0
  2. admin.html +227 -0
  3. admin.js +437 -0
admin.css ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ SpeakFlow AI – admin.css
3
+ Professional Dark Admin Dashboard
4
+ ============================================================ */
5
+ :root{
6
+ --blue:#2563EB;--blue-light:#3B82F6;--blue-dark:#1D4ED8;
7
+ --teal:#14B8A6;--purple:#7C3AED;--green:#10B981;--orange:#F59E0B;--red:#EF4444;
8
+ --bg:#0B1120;--sidebar-bg:#0F172A;--surface:rgba(30,41,59,0.85);
9
+ --surface-2:#1E293B;--surface-3:#263148;
10
+ --border:rgba(148,163,184,0.15);--border-solid:#334155;
11
+ --text:#F1F5F9;--text-muted:#94A3B8;--text-light:#64748B;
12
+ --radius:14px;--radius-lg:20px;
13
+ --shadow:0 4px 20px rgba(0,0,0,.35);--shadow-lg:0 20px 60px rgba(0,0,0,.5);
14
+ --font-display:'Space Grotesk',sans-serif;--font-body:'Inter',sans-serif;
15
+ --tr:.25s ease;
16
+ --sidebar-w:260px;
17
+ }
18
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
19
+ html{scroll-behavior:smooth;}
20
+ body{font-family:var(--font-body);background:var(--bg);color:var(--text);display:flex;min-height:100vh;overflow-x:hidden;}
21
+ a{color:inherit;text-decoration:none;}
22
+ button{cursor:pointer;font-family:var(--font-body);border:none;background:none;}
23
+ .logo-ai{background:linear-gradient(135deg,#3B82F6,#14B8A6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
24
+
25
+ /* ── Sidebar ─────────────────────────────────────────────── */
26
+ .sidebar{width:var(--sidebar-w);background:var(--sidebar-bg);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;height:100vh;z-index:100;transition:transform var(--tr);}
27
+ .sidebar-logo{display:flex;align-items:center;gap:12px;padding:24px 20px;border-bottom:1px solid var(--border);}
28
+ .sidebar-logo-icon{width:44px;height:44px;background:linear-gradient(135deg,var(--blue),var(--teal));border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:#fff;flex-shrink:0;}
29
+ .sidebar-logo-text{font-family:var(--font-display);font-size:1.1rem;font-weight:700;display:block;}
30
+ .sidebar-logo-sub{font-size:.72rem;color:var(--text-muted);margin-top:2px;}
31
+ .sidebar-nav{flex:1;padding:16px 12px;display:flex;flex-direction:column;gap:4px;overflow-y:auto;}
32
+ .nav-item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:11px;font-size:.9rem;font-weight:500;color:var(--text-muted);transition:all var(--tr);}
33
+ .nav-item i{width:18px;text-align:center;font-size:1rem;}
34
+ .nav-item:hover{background:rgba(255,255,255,.05);color:var(--text);}
35
+ .nav-item.active{background:linear-gradient(135deg,rgba(37,99,235,.25),rgba(20,184,166,.15));color:var(--blue-light);border-left:3px solid var(--blue);}
36
+ .sidebar-footer{padding:16px 20px;border-top:1px solid var(--border);}
37
+ .admin-profile{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
38
+ .admin-avatar{width:38px;height:38px;background:linear-gradient(135deg,var(--purple),var(--blue));border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.95rem;color:#fff;flex-shrink:0;}
39
+ .admin-profile strong{font-size:.88rem;display:block;}
40
+ .admin-profile p{font-size:.73rem;color:var(--text-muted);}
41
+ .btn-logout-sidebar{width:100%;padding:10px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.2);border-radius:10px;color:#F87171;font-size:.88rem;font-weight:600;display:flex;align-items:center;justify-content:center;gap:8px;transition:all var(--tr);}
42
+ .btn-logout-sidebar:hover{background:rgba(239,68,68,.2);}
43
+
44
+ /* ── Main ────────────────────────────────────────────────── */
45
+ .admin-main{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;min-height:100vh;}
46
+
47
+ /* ── Topbar ──────────────────────────────────────────────── */
48
+ .topbar{display:flex;align-items:center;gap:16px;padding:0 28px;height:64px;background:var(--sidebar-bg);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:50;}
49
+ .sidebar-toggle{display:none;background:rgba(255,255,255,.07);border-radius:8px;width:38px;height:38px;align-items:center;justify-content:center;font-size:1rem;color:var(--text-muted);}
50
+ .topbar-title{font-family:var(--font-display);font-size:1rem;font-weight:600;flex:1;}
51
+ .topbar-right{display:flex;align-items:center;gap:14px;}
52
+ .topbar-badge{background:rgba(124,58,237,.2);color:#A78BFA;padding:5px 12px;border-radius:50px;font-size:.78rem;font-weight:700;display:flex;align-items:center;gap:6px;}
53
+ .topbar-time{font-size:.82rem;color:var(--text-muted);}
54
+
55
+ /* ── Tab Sections ────────────────────────────────────────── */
56
+ .tab-section{display:none;padding:28px;animation:fadeIn .3s ease;}
57
+ .tab-section.active{display:block;}
58
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
59
+ .section-heading{margin-bottom:24px;}
60
+ .section-heading h2{font-family:var(--font-display);font-size:1.35rem;font-weight:700;display:flex;align-items:center;gap:10px;margin-bottom:4px;}
61
+ .section-heading p{color:var(--text-muted);font-size:.88rem;}
62
+
63
+ /* ── Stats Cards ─────────────────────────────────────────── */
64
+ .stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px;}
65
+ .stat-card{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);padding:20px;display:flex;align-items:center;gap:16px;position:relative;overflow:hidden;transition:transform var(--tr),box-shadow var(--tr);}
66
+ .stat-card::before{content:'';position:absolute;inset:0;opacity:.07;border-radius:var(--radius);}
67
+ .stat-card:hover{transform:translateY(-3px);box-shadow:var(--shadow);}
68
+ .card-blue .stat-icon{background:rgba(37,99,235,.2);color:var(--blue-light);}
69
+ .card-blue::before{background:var(--blue);}
70
+ .card-teal .stat-icon{background:rgba(20,184,166,.2);color:var(--teal);}
71
+ .card-teal::before{background:var(--teal);}
72
+ .card-green .stat-icon{background:rgba(16,185,129,.2);color:var(--green);}
73
+ .card-green::before{background:var(--green);}
74
+ .card-orange .stat-icon{background:rgba(245,158,11,.2);color:var(--orange);}
75
+ .card-orange::before{background:var(--orange);}
76
+ .stat-icon{width:50px;height:50px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.3rem;flex-shrink:0;}
77
+ .stat-number{font-family:var(--font-display);font-size:1.8rem;font-weight:700;line-height:1;}
78
+ .stat-label{font-size:.8rem;color:var(--text-muted);margin-top:4px;}
79
+
80
+ /* ── Charts ──────────────────────────────────────────────── */
81
+ .quick-charts{display:grid;grid-template-columns:1fr 2fr;gap:16px;}
82
+ .charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
83
+ .chart-card{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);padding:20px;}
84
+ .chart-wide{grid-column:span 1;}
85
+ .chart-full{grid-column:1/-1;}
86
+ .chart-title{font-family:var(--font-display);font-size:.95rem;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px;color:var(--text);}
87
+ .chart-title i{color:var(--blue-light);}
88
+ .chart-wrap{position:relative;height:220px;display:flex;align-items:center;justify-content:center;}
89
+ .chart-tall{height:280px;}
90
+
91
+ /* ── Table ───────────────────────────────────────────────── */
92
+ .table-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap;}
93
+ .search-box{display:flex;align-items:center;gap:10px;background:var(--surface-2);border:1px solid var(--border-solid);border-radius:10px;padding:10px 14px;flex:1;min-width:200px;}
94
+ .search-box i{color:var(--text-muted);}
95
+ .search-box input{background:none;border:none;outline:none;color:var(--text);font-size:.9rem;font-family:var(--font-body);width:100%;}
96
+ .search-box input::placeholder{color:var(--text-light);}
97
+ .count-badge{background:rgba(37,99,235,.15);color:var(--blue-light);padding:6px 14px;border-radius:50px;font-size:.8rem;font-weight:600;white-space:nowrap;}
98
+ .btn-export{background:linear-gradient(135deg,var(--green),#059669);color:#fff;padding:10px 18px;border-radius:10px;font-size:.85rem;font-weight:700;display:flex;align-items:center;gap:7px;transition:all var(--tr);}
99
+ .btn-export:hover{transform:translateY(-2px);box-shadow:0 4px 16px rgba(16,185,129,.3);}
100
+ .table-wrap{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius);overflow:auto;}
101
+ .admin-table{width:100%;border-collapse:collapse;}
102
+ .admin-table th{padding:13px 16px;text-align:left;font-size:.78rem;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border-solid);background:rgba(255,255,255,.02);white-space:nowrap;}
103
+ .admin-table td{padding:13px 16px;font-size:.88rem;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle;}
104
+ .admin-table tbody tr:last-child td{border-bottom:none;}
105
+ .admin-table tbody tr:hover td{background:rgba(255,255,255,.03);}
106
+ .table-empty{text-align:center;padding:48px;color:var(--text-muted);}
107
+ .table-empty i{font-size:2rem;margin-bottom:12px;opacity:.4;}
108
+ .table-empty p{font-size:.9rem;}
109
+
110
+ /* ── Badges ──────────────────────────────────────────────── */
111
+ .badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:50px;font-size:.75rem;font-weight:700;}
112
+ .badge-green{background:rgba(16,185,129,.15);color:var(--green);}
113
+ .badge-orange{background:rgba(245,158,11,.15);color:var(--orange);}
114
+ .badge-blue{background:rgba(37,99,235,.15);color:var(--blue-light);}
115
+
116
+ /* ── Action Buttons ─────────────────��────────────────────── */
117
+ .btn-tbl{padding:5px 10px;border-radius:7px;font-size:.78rem;font-weight:600;display:inline-flex;align-items:center;gap:5px;transition:all var(--tr);}
118
+ .btn-view{background:rgba(37,99,235,.15);color:var(--blue-light);}
119
+ .btn-view:hover{background:rgba(37,99,235,.3);}
120
+ .btn-delete{background:rgba(239,68,68,.12);color:#F87171;}
121
+ .btn-delete:hover{background:rgba(239,68,68,.25);}
122
+
123
+ /* ── System Stats ────────────────────────────────────────── */
124
+ .sys-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
125
+ .sys-stat-item{background:var(--surface-3);border-radius:12px;padding:16px;text-align:center;}
126
+ .sys-stat-val{font-family:var(--font-display);font-size:1.4rem;font-weight:700;margin-bottom:4px;}
127
+ .sys-stat-lbl{font-size:.75rem;color:var(--text-muted);}
128
+
129
+ /* ── Modal ───────────────────────────────────────────────── */
130
+ .modal-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:16px;}
131
+ .modal-box{background:var(--surface-2);border:1px solid var(--border-solid);border-radius:var(--radius-lg);padding:28px;width:100%;max-width:460px;box-shadow:var(--shadow-lg);}
132
+ .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;}
133
+ .modal-header h3{font-size:1.05rem;font-weight:700;display:flex;align-items:center;gap:9px;}
134
+ .modal-close{background:rgba(255,255,255,.07);border:none;color:var(--text-muted);width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;}
135
+ .modal-close:hover{background:rgba(255,255,255,.14);}
136
+ .user-detail-row{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border-solid);font-size:.9rem;}
137
+ .user-detail-row:last-child{border-bottom:none;}
138
+ .user-detail-row span:first-child{color:var(--text-muted);}
139
+
140
+ /* ── Responsive ──────────────────────────────────────────── */
141
+ @media(max-width:1100px){.stats-grid{grid-template-columns:repeat(2,1fr);}
142
+ .quick-charts,.charts-grid{grid-template-columns:1fr;}.chart-wide,.chart-full{grid-column:1;}
143
+ .sys-stats-grid{grid-template-columns:repeat(2,1fr);}}
144
+ @media(max-width:768px){
145
+ .sidebar{transform:translateX(-100%);}.sidebar.open{transform:translateX(0);}
146
+ .admin-main{margin-left:0;}.sidebar-toggle{display:flex;}
147
+ .tab-section{padding:16px;}.stats-grid{grid-template-columns:1fr 1fr;}
148
+ .topbar{padding:0 16px;}
149
+ }
150
+ @media(max-width:480px){.stats-grid{grid-template-columns:1fr;}.sys-stats-grid{grid-template-columns:1fr 1fr;}}
admin.html ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Admin Dashboard – SpeakFlow AI</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
10
+ <link rel="stylesheet" href="admin.css" />
11
+ </head>
12
+ <body>
13
+
14
+ <!-- ── Sidebar ──────────────────────────────────────────── -->
15
+ <aside class="sidebar" id="sidebar">
16
+ <div class="sidebar-logo">
17
+ <div class="sidebar-logo-icon"><i class="fas fa-wave-square"></i></div>
18
+ <div>
19
+ <span class="sidebar-logo-text">SpeakFlow <span class="logo-ai">AI</span></span>
20
+ <p class="sidebar-logo-sub">Admin Panel</p>
21
+ </div>
22
+ </div>
23
+
24
+ <nav class="sidebar-nav">
25
+ <a href="#" class="nav-item active" data-tab="dashboard">
26
+ <i class="fas fa-tachometer-alt"></i> <span>Dashboard</span>
27
+ </a>
28
+ <a href="#" class="nav-item" data-tab="users">
29
+ <i class="fas fa-users"></i> <span>User Management</span>
30
+ </a>
31
+ <a href="#" class="nav-item" data-tab="analyses">
32
+ <i class="fas fa-history"></i> <span>Analysis History</span>
33
+ </a>
34
+ <a href="#" class="nav-item" data-tab="charts">
35
+ <i class="fas fa-chart-line"></i> <span>Analytics Charts</span>
36
+ </a>
37
+ </nav>
38
+
39
+ <div class="sidebar-footer">
40
+ <div class="admin-profile">
41
+ <div class="admin-avatar"><i class="fas fa-shield-alt"></i></div>
42
+ <div>
43
+ <strong>Administrator</strong>
44
+ <p>admin@speakflow.ai</p>
45
+ </div>
46
+ </div>
47
+ <button class="btn-logout-sidebar" id="adminLogout">
48
+ <i class="fas fa-sign-out-alt"></i> Logout
49
+ </button>
50
+ </div>
51
+ </aside>
52
+
53
+ <!-- ── Main Content ─────────────────────────────────────── -->
54
+ <main class="admin-main">
55
+
56
+ <!-- Top Bar -->
57
+ <div class="topbar">
58
+ <button class="sidebar-toggle" id="sidebarToggle"><i class="fas fa-bars"></i></button>
59
+ <div class="topbar-title" id="topbarTitle">Dashboard Overview</div>
60
+ <div class="topbar-right">
61
+ <div class="topbar-badge"><i class="fas fa-shield-alt"></i> Admin</div>
62
+ <div class="topbar-time" id="topbarTime"></div>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- ═══ DASHBOARD TAB ════════════════════════════════════ -->
67
+ <section class="tab-section active" id="tab-dashboard">
68
+ <div class="section-heading">
69
+ <h2><i class="fas fa-tachometer-alt"></i> Dashboard Overview</h2>
70
+ <p>Real-time platform statistics and system health.</p>
71
+ </div>
72
+
73
+ <!-- Stats Cards -->
74
+ <div class="stats-grid" id="statsGrid">
75
+ <div class="stat-card card-blue">
76
+ <div class="stat-icon"><i class="fas fa-users"></i></div>
77
+ <div class="stat-body">
78
+ <div class="stat-number" id="sc-users">0</div>
79
+ <div class="stat-label">Total Users</div>
80
+ </div>
81
+ <div class="stat-trend" id="st-users"></div>
82
+ </div>
83
+ <div class="stat-card card-teal">
84
+ <div class="stat-icon"><i class="fas fa-brain"></i></div>
85
+ <div class="stat-body">
86
+ <div class="stat-number" id="sc-analyses">0</div>
87
+ <div class="stat-label">Total Analyses</div>
88
+ </div>
89
+ </div>
90
+ <div class="stat-card card-green">
91
+ <div class="stat-icon"><i class="fas fa-check-circle"></i></div>
92
+ <div class="stat-body">
93
+ <div class="stat-number" id="sc-normal">0</div>
94
+ <div class="stat-label">Normal Detections</div>
95
+ </div>
96
+ </div>
97
+ <div class="stat-card card-orange">
98
+ <div class="stat-icon"><i class="fas fa-exclamation-triangle"></i></div>
99
+ <div class="stat-body">
100
+ <div class="stat-number" id="sc-stutter">0</div>
101
+ <div class="stat-label">Stutter Detections</div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Quick Charts Row -->
107
+ <div class="quick-charts">
108
+ <div class="chart-card">
109
+ <h3 class="chart-title"><i class="fas fa-chart-pie"></i> Detection Ratio</h3>
110
+ <div class="chart-wrap"><canvas id="dashPieChart"></canvas></div>
111
+ </div>
112
+ <div class="chart-card chart-wide">
113
+ <h3 class="chart-title"><i class="fas fa-chart-bar"></i> Recent Activity (Last 7 Days)</h3>
114
+ <div class="chart-wrap"><canvas id="dashBarChart"></canvas></div>
115
+ </div>
116
+ </div>
117
+ </section>
118
+
119
+ <!-- ═══ USERS TAB ════════════════════════════════════════ -->
120
+ <section class="tab-section" id="tab-users">
121
+ <div class="section-heading">
122
+ <h2><i class="fas fa-users"></i> User Management</h2>
123
+ <p>View, search, and manage registered users.</p>
124
+ </div>
125
+ <div class="table-toolbar">
126
+ <div class="search-box">
127
+ <i class="fas fa-search"></i>
128
+ <input type="text" id="userSearch" placeholder="Search by name or email..." />
129
+ </div>
130
+ <span id="userCount" class="count-badge">0 users</span>
131
+ </div>
132
+ <div class="table-wrap">
133
+ <table class="admin-table" id="usersTable">
134
+ <thead>
135
+ <tr>
136
+ <th>#</th>
137
+ <th>Name</th>
138
+ <th>Email</th>
139
+ <th>Registered</th>
140
+ <th>Analyses</th>
141
+ <th>Actions</th>
142
+ </tr>
143
+ </thead>
144
+ <tbody id="usersBody"></tbody>
145
+ </table>
146
+ <div class="table-empty" id="usersEmpty" style="display:none;">
147
+ <i class="fas fa-user-slash"></i><p>No users found.</p>
148
+ </div>
149
+ </div>
150
+ </section>
151
+
152
+ <!-- ═══ ANALYSES TAB ═════════════════════════════════════ -->
153
+ <section class="tab-section" id="tab-analyses">
154
+ <div class="section-heading">
155
+ <h2><i class="fas fa-history"></i> Analysis History</h2>
156
+ <p>All speech analyses performed across all users.</p>
157
+ </div>
158
+ <div class="table-toolbar">
159
+ <div class="search-box">
160
+ <i class="fas fa-search"></i>
161
+ <input type="text" id="analysisSearch" placeholder="Search by user, file, or result..." />
162
+ </div>
163
+ <button class="btn-export" id="exportCsvBtn"><i class="fas fa-file-csv"></i> Export CSV</button>
164
+ </div>
165
+ <div class="table-wrap">
166
+ <table class="admin-table" id="analysesTable">
167
+ <thead>
168
+ <tr>
169
+ <th>#</th>
170
+ <th>User</th>
171
+ <th>File Name</th>
172
+ <th>Date</th>
173
+ <th>Result</th>
174
+ <th>Confidence</th>
175
+ <th>Actions</th>
176
+ </tr>
177
+ </thead>
178
+ <tbody id="analysesBody"></tbody>
179
+ </table>
180
+ <div class="table-empty" id="analysesEmpty" style="display:none;">
181
+ <i class="fas fa-history"></i><p>No analyses found.</p>
182
+ </div>
183
+ </div>
184
+ </section>
185
+
186
+ <!-- ═══ CHARTS TAB ════════════════════════════════════════ -->
187
+ <section class="tab-section" id="tab-charts">
188
+ <div class="section-heading">
189
+ <h2><i class="fas fa-chart-line"></i> Analytics Charts</h2>
190
+ <p>Visual insights into platform usage and detection trends.</p>
191
+ </div>
192
+ <div class="charts-grid">
193
+ <div class="chart-card chart-full">
194
+ <h3 class="chart-title"><i class="fas fa-chart-line"></i> User Growth Over Time</h3>
195
+ <div class="chart-wrap chart-tall"><canvas id="userGrowthChart"></canvas></div>
196
+ </div>
197
+ <div class="chart-card">
198
+ <h3 class="chart-title"><i class="fas fa-chart-pie"></i> Normal vs Stutter</h3>
199
+ <div class="chart-wrap"><canvas id="detectionPieChart"></canvas></div>
200
+ </div>
201
+ <div class="chart-card">
202
+ <h3 class="chart-title"><i class="fas fa-chart-bar"></i> Analysis Activity</h3>
203
+ <div class="chart-wrap"><canvas id="activityChart"></canvas></div>
204
+ </div>
205
+ <div class="chart-card chart-full">
206
+ <h3 class="chart-title"><i class="fas fa-server"></i> System Statistics</h3>
207
+ <div class="sys-stats-grid" id="sysStatsGrid"></div>
208
+ </div>
209
+ </div>
210
+ </section>
211
+
212
+ </main>
213
+
214
+ <!-- User View Modal -->
215
+ <div id="userViewModal" class="modal-overlay" style="display:none;">
216
+ <div class="modal-box glass-card">
217
+ <div class="modal-header">
218
+ <h3><i class="fas fa-user"></i> User Details</h3>
219
+ <button class="modal-close" id="userViewClose"><i class="fas fa-times"></i></button>
220
+ </div>
221
+ <div id="userViewBody"></div>
222
+ </div>
223
+ </div>
224
+
225
+ <script src="admin.js"></script>
226
+ </body>
227
+ </html>
admin.js ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ SpeakFlow AI – admin.js
3
+ Full Admin Dashboard Logic
4
+ ============================================================ */
5
+ 'use strict';
6
+
7
+ /* ── Auth Guard ─────────────────────────────────────────────── */
8
+ const session = JSON.parse(localStorage.getItem('sf_session') || 'null');
9
+ if (!session || !session.loggedIn || !session.isAdmin) {
10
+ window.location.href = 'login.html';
11
+ }
12
+
13
+ /* ── Seed demo data if empty ───────────────────────────────── */
14
+ (function seedDemoData() {
15
+ const users = JSON.parse(localStorage.getItem('sf_users') || '[]');
16
+ if (users.length === 0) {
17
+ const demoUsers = [
18
+ { id: 'usr_001', name: 'Alice Johnson', email: 'alice@example.com', password: 'pass123', createdAt: daysAgo(30) },
19
+ { id: 'usr_002', name: 'Bob Martinez', email: 'bob@example.com', password: 'pass123', createdAt: daysAgo(25) },
20
+ { id: 'usr_003', name: 'Carol Williams', email: 'carol@example.com', password: 'pass123', createdAt: daysAgo(18) },
21
+ { id: 'usr_004', name: 'David Chen', email: 'david@example.com', password: 'pass123', createdAt: daysAgo(10) },
22
+ { id: 'usr_005', name: 'Eva Brown', email: 'eva@example.com', password: 'pass123', createdAt: daysAgo(4) },
23
+ ];
24
+ localStorage.setItem('sf_users', JSON.stringify(demoUsers));
25
+ }
26
+
27
+ const history = JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
28
+ if (history.length === 0) {
29
+ const files = ['speech_sample.wav','recording_01.mp3','audio_test.wav','voice_clip.mp3','session_audio.wav'];
30
+ const results= ['Normal','Stutter Detected','Normal','Normal','Stutter Detected'];
31
+ const confs = [92, 87, 95, 78, 83];
32
+ const stored = JSON.parse(localStorage.getItem('sf_users') || '[]');
33
+ const demo = stored.length ? stored : [{ id:'usr_001', name:'Alice Johnson', email:'alice@example.com' }];
34
+ const entries = [];
35
+ for (let i = 0; i < 12; i++) {
36
+ const u = demo[i % demo.length];
37
+ entries.push({
38
+ id: 'ana_' + Date.now() + i,
39
+ userId: u.id,
40
+ userName: u.name,
41
+ userEmail: u.email,
42
+ fileName: files[i % files.length],
43
+ date: daysAgo(Math.floor(Math.random() * 28)),
44
+ result: results[i % results.length],
45
+ confidence: confs[i % confs.length] + Math.floor(Math.random() * 5),
46
+ });
47
+ }
48
+ localStorage.setItem('sf_analysis_history', JSON.stringify(entries));
49
+ }
50
+ })();
51
+
52
+ function daysAgo(n) {
53
+ const d = new Date();
54
+ d.setDate(d.getDate() - n);
55
+ return d.toISOString();
56
+ }
57
+
58
+ /* ── DOM helpers ─────────────────────────────────────────── */
59
+ const $ = id => document.getElementById(id);
60
+ const users = () => JSON.parse(localStorage.getItem('sf_users') || '[]');
61
+ const analyses = () => JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
62
+
63
+ /* ── Clock ───────────────────────────────────────────────── */
64
+ function updateClock() {
65
+ const now = new Date();
66
+ $('topbarTime').textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
67
+ }
68
+ setInterval(updateClock, 1000);
69
+ updateClock();
70
+
71
+ /* ── Sidebar Toggle ──────────────────────────────────────── */
72
+ $('sidebarToggle').addEventListener('click', () => {
73
+ document.getElementById('sidebar').classList.toggle('open');
74
+ });
75
+
76
+ /* ── Logout ──────────────────────────────────────────────── */
77
+ $('adminLogout').addEventListener('click', () => {
78
+ localStorage.removeItem('sf_session');
79
+ window.location.href = 'login.html';
80
+ });
81
+
82
+ /* ── Tab Navigation ──────────────────────────────────────── */
83
+ const tabTitles = {
84
+ dashboard: 'Dashboard Overview',
85
+ users: 'User Management',
86
+ analyses: 'Analysis History',
87
+ charts: 'Analytics Charts',
88
+ };
89
+
90
+ document.querySelectorAll('.nav-item').forEach(item => {
91
+ item.addEventListener('click', e => {
92
+ e.preventDefault();
93
+ const tab = item.dataset.tab;
94
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
95
+ document.querySelectorAll('.tab-section').forEach(s => s.classList.remove('active'));
96
+ item.classList.add('active');
97
+ $('tab-' + tab).classList.add('active');
98
+ $('topbarTitle').textContent = tabTitles[tab];
99
+ if (tab === 'charts') renderChartsTab();
100
+ if (tab === 'users') renderUsersTable(users());
101
+ if (tab === 'analyses') renderAnalysesTable(analyses());
102
+ });
103
+ });
104
+
105
+ /* ═══════════════════════════════════════════════════════════
106
+ DASHBOARD
107
+ ═══════════════════════════════════════════════════════════ */
108
+ function renderDashboard() {
109
+ const u = users();
110
+ const a = analyses();
111
+ const normal = a.filter(x => x.result === 'Normal').length;
112
+ const stutter = a.filter(x => x.result !== 'Normal').length;
113
+
114
+ animateCount('sc-users', u.length);
115
+ animateCount('sc-analyses', a.length);
116
+ animateCount('sc-normal', normal);
117
+ animateCount('sc-stutter', stutter);
118
+
119
+ renderDashPie(normal, stutter);
120
+ renderDashBar(a);
121
+ }
122
+
123
+ function animateCount(id, target) {
124
+ const el = $(id);
125
+ let current = 0;
126
+ const step = Math.max(1, Math.ceil(target / 40));
127
+ const timer = setInterval(() => {
128
+ current = Math.min(current + step, target);
129
+ el.textContent = current;
130
+ if (current >= target) clearInterval(timer);
131
+ }, 30);
132
+ }
133
+
134
+ /* Dash Pie */
135
+ let dashPieInst = null;
136
+ function renderDashPie(normal, stutter) {
137
+ const ctx = $('dashPieChart').getContext('2d');
138
+ if (dashPieInst) dashPieInst.destroy();
139
+ dashPieInst = new Chart(ctx, {
140
+ type: 'doughnut',
141
+ data: {
142
+ labels: ['Normal', 'Stutter'],
143
+ datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 8 }]
144
+ },
145
+ options: {
146
+ responsive: true, maintainAspectRatio: false,
147
+ plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', font: { size: 12 }, padding: 16 } } }
148
+ }
149
+ });
150
+ }
151
+
152
+ /* Dash Bar – last 7 days */
153
+ let dashBarInst = null;
154
+ function renderDashBar(a) {
155
+ const days = [];
156
+ const counts = [];
157
+ for (let i = 6; i >= 0; i--) {
158
+ const d = new Date();
159
+ d.setDate(d.getDate() - i);
160
+ const label = d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' });
161
+ days.push(label);
162
+ const ds = d.toDateString();
163
+ counts.push(a.filter(x => new Date(x.date).toDateString() === ds).length);
164
+ }
165
+ const ctx = $('dashBarChart').getContext('2d');
166
+ if (dashBarInst) dashBarInst.destroy();
167
+ dashBarInst = new Chart(ctx, {
168
+ type: 'bar',
169
+ data: {
170
+ labels: days,
171
+ datasets: [{ label: 'Analyses', data: counts, backgroundColor: 'rgba(59,130,246,0.7)', borderRadius: 8, borderSkipped: false }]
172
+ },
173
+ options: {
174
+ responsive: true, maintainAspectRatio: false,
175
+ plugins: { legend: { display: false } },
176
+ scales: {
177
+ x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,.04)' } },
178
+ y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
179
+ }
180
+ }
181
+ });
182
+ }
183
+
184
+ /* ═══════════════════════════════════════════════════════════
185
+ USERS TABLE
186
+ ═══════════════════════════════════════════════════════════ */
187
+ function renderUsersTable(list) {
188
+ const body = $('usersBody');
189
+ const empty = $('usersEmpty');
190
+ const a = analyses();
191
+ body.innerHTML = '';
192
+ $('userCount').textContent = list.length + ' user' + (list.length !== 1 ? 's' : '');
193
+
194
+ if (!list.length) { empty.style.display = 'block'; return; }
195
+ empty.style.display = 'none';
196
+
197
+ list.forEach((u, i) => {
198
+ const userAna = a.filter(x => x.userEmail === u.email).length;
199
+ const tr = document.createElement('tr');
200
+ tr.innerHTML = `
201
+ <td><span style="color:var(--text-muted)">${i + 1}</span></td>
202
+ <td><strong>${esc(u.name)}</strong></td>
203
+ <td style="color:var(--text-muted)">${esc(u.email)}</td>
204
+ <td style="color:var(--text-muted)">${new Date(u.createdAt).toLocaleDateString()}</td>
205
+ <td><span class="badge badge-blue">${userAna}</span></td>
206
+ <td style="display:flex;gap:6px;flex-wrap:wrap;">
207
+ <button class="btn-tbl btn-view" data-id="${u.id}"><i class="fas fa-eye"></i> View</button>
208
+ <button class="btn-tbl btn-delete" data-id="${u.id}"><i class="fas fa-trash"></i> Delete</button>
209
+ </td>`;
210
+ body.appendChild(tr);
211
+ });
212
+
213
+ body.querySelectorAll('.btn-view').forEach(btn => btn.addEventListener('click', () => openUserView(btn.dataset.id)));
214
+ body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteUser(btn.dataset.id)));
215
+ }
216
+
217
+ $('userSearch').addEventListener('input', function () {
218
+ const q = this.value.toLowerCase();
219
+ renderUsersTable(users().filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)));
220
+ });
221
+
222
+ function deleteUser(id) {
223
+ if (!confirm('Delete this user? This cannot be undone.')) return;
224
+ let u = users().filter(x => x.id !== id);
225
+ localStorage.setItem('sf_users', JSON.stringify(u));
226
+ // Also remove their analyses
227
+ let a = analyses().filter(x => {
228
+ const del = JSON.parse(localStorage.getItem('sf_users') || '[]');
229
+ return x.userId !== id;
230
+ });
231
+ localStorage.setItem('sf_analysis_history', JSON.stringify(analyses().filter(x => x.userId !== id)));
232
+ renderUsersTable(u);
233
+ renderDashboard();
234
+ }
235
+
236
+ function openUserView(id) {
237
+ const u = users().find(x => x.id === id);
238
+ if (!u) return;
239
+ const a = analyses().filter(x => x.userEmail === u.email);
240
+ const nb = $('userViewBody');
241
+ nb.innerHTML = `
242
+ <div class="user-detail-row"><span><i class="fas fa-user" style="width:18px;text-align:center;"></i> Full Name</span><strong>${esc(u.name)}</strong></div>
243
+ <div class="user-detail-row"><span><i class="fas fa-envelope" style="width:18px;text-align:center;"></i> Email</span><strong>${esc(u.email)}</strong></div>
244
+ <div class="user-detail-row"><span><i class="fas fa-calendar" style="width:18px;text-align:center;"></i> Registered</span><strong>${new Date(u.createdAt).toLocaleString()}</strong></div>
245
+ <div class="user-detail-row"><span><i class="fas fa-chart-bar" style="width:18px;text-align:center;"></i> Total Analyses</span><strong>${a.length}</strong></div>
246
+ <div class="user-detail-row"><span><i class="fas fa-check-circle" style="width:18px;text-align:center;"></i> Normal Results</span><strong style="color:var(--green)">${a.filter(x=>x.result==='Normal').length}</strong></div>
247
+ <div class="user-detail-row"><span><i class="fas fa-exclamation-triangle" style="width:18px;text-align:center;"></i> Stutter Results</span><strong style="color:var(--orange)">${a.filter(x=>x.result!=='Normal').length}</strong></div>
248
+ `;
249
+ $('userViewModal').style.display = 'flex';
250
+ }
251
+
252
+ $('userViewClose').addEventListener('click', () => { $('userViewModal').style.display = 'none'; });
253
+ $('userViewModal').addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
254
+
255
+ /* ═══════════════════════════════════════════════════════════
256
+ ANALYSES TABLE
257
+ ═══════════════════════════════════════════════════════════ */
258
+ function renderAnalysesTable(list) {
259
+ const body = $('analysesBody');
260
+ const empty = $('analysesEmpty');
261
+ body.innerHTML = '';
262
+ if (!list.length) { empty.style.display = 'block'; return; }
263
+ empty.style.display = 'none';
264
+
265
+ list.forEach((a, i) => {
266
+ const isNormal = a.result === 'Normal';
267
+ const tr = document.createElement('tr');
268
+ tr.innerHTML = `
269
+ <td><span style="color:var(--text-muted)">${i + 1}</span></td>
270
+ <td><strong>${esc(a.userName || 'Unknown')}</strong></td>
271
+ <td style="color:var(--text-muted);font-size:.83rem">${esc(a.fileName || 'N/A')}</td>
272
+ <td style="color:var(--text-muted)">${new Date(a.date).toLocaleDateString()}</td>
273
+ <td><span class="badge ${isNormal ? 'badge-green' : 'badge-orange'}"><i class="fas fa-${isNormal ? 'check-circle' : 'exclamation-triangle'}"></i> ${esc(a.result)}</span></td>
274
+ <td><strong>${a.confidence || '--'}%</strong></td>
275
+ <td><button class="btn-tbl btn-delete" data-id="${a.id}"><i class="fas fa-trash"></i> Delete</button></td>`;
276
+ body.appendChild(tr);
277
+ });
278
+
279
+ body.querySelectorAll('.btn-delete').forEach(btn => btn.addEventListener('click', () => deleteAnalysis(btn.dataset.id)));
280
+ }
281
+
282
+ $('analysisSearch').addEventListener('input', function () {
283
+ const q = this.value.toLowerCase();
284
+ renderAnalysesTable(analyses().filter(a =>
285
+ (a.userName || '').toLowerCase().includes(q) ||
286
+ (a.fileName || '').toLowerCase().includes(q) ||
287
+ (a.result || '').toLowerCase().includes(q)
288
+ ));
289
+ });
290
+
291
+ function deleteAnalysis(id) {
292
+ if (!confirm('Delete this analysis record?')) return;
293
+ const updated = analyses().filter(x => x.id !== id);
294
+ localStorage.setItem('sf_analysis_history', JSON.stringify(updated));
295
+ renderAnalysesTable(updated);
296
+ renderDashboard();
297
+ }
298
+
299
+ /* ── Export CSV ──────────────────────────────────────────── */
300
+ $('exportCsvBtn').addEventListener('click', () => {
301
+ const a = analyses();
302
+ if (!a.length) { alert('No analysis data to export.'); return; }
303
+ const header = ['#','User Name','User Email','File Name','Date','Result','Confidence'];
304
+ const rows = a.map((x, i) => [
305
+ i + 1, `"${x.userName || ''}"`, `"${x.userEmail || ''}"`,
306
+ `"${x.fileName || ''}"`, new Date(x.date).toLocaleString(),
307
+ x.result, x.confidence + '%'
308
+ ]);
309
+ const csv = [header.join(','), ...rows.map(r => r.join(','))].join('\n');
310
+ const blob = new Blob([csv], { type: 'text/csv' });
311
+ const url = URL.createObjectURL(blob);
312
+ const link = document.createElement('a');
313
+ link.href = url; link.download = 'speakflow_analyses.csv';
314
+ link.click(); URL.revokeObjectURL(url);
315
+ });
316
+
317
+ /* ═══════════════════════════════════════════════════════════
318
+ CHARTS TAB
319
+ ═══════════════════════════════════════════════════════════ */
320
+ let chartsRendered = false;
321
+ let growthInst = null, pieInst = null, activityInst = null;
322
+
323
+ function renderChartsTab() {
324
+ if (chartsRendered) { chartsRendered = false; } // always re-render fresh
325
+ const a = analyses();
326
+ const u = users();
327
+
328
+ // 1. User Growth Line Chart
329
+ const growthCtx = $('userGrowthChart').getContext('2d');
330
+ if (growthInst) growthInst.destroy();
331
+
332
+ const months = getLast6Months();
333
+ const growthData = months.map(m => u.filter(x => {
334
+ const d = new Date(x.createdAt);
335
+ return d.getFullYear() === m.year && d.getMonth() === m.month;
336
+ }).length);
337
+
338
+ growthInst = new Chart(growthCtx, {
339
+ type: 'line',
340
+ data: {
341
+ labels: months.map(m => m.label),
342
+ datasets: [{
343
+ label: 'New Users', data: growthData,
344
+ borderColor: '#3B82F6', backgroundColor: 'rgba(59,130,246,0.12)',
345
+ fill: true, tension: 0.4, pointBackgroundColor: '#3B82F6', pointRadius: 5
346
+ }]
347
+ },
348
+ options: {
349
+ responsive: true, maintainAspectRatio: false,
350
+ plugins: { legend: { labels: { color: '#94A3B8' } } },
351
+ scales: {
352
+ x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } },
353
+ y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
354
+ }
355
+ }
356
+ });
357
+
358
+ // 2. Detection Pie
359
+ const normal = a.filter(x => x.result === 'Normal').length;
360
+ const stutter = a.filter(x => x.result !== 'Normal').length;
361
+ const pieCtx = $('detectionPieChart').getContext('2d');
362
+ if (pieInst) pieInst.destroy();
363
+ pieInst = new Chart(pieCtx, {
364
+ type: 'pie',
365
+ data: {
366
+ labels: ['Normal', 'Stutter Detected'],
367
+ datasets: [{ data: [normal || 1, stutter || 0], backgroundColor: ['#10B981','#F59E0B'], borderWidth: 0, hoverOffset: 10 }]
368
+ },
369
+ options: {
370
+ responsive: true, maintainAspectRatio: false,
371
+ plugins: { legend: { position: 'bottom', labels: { color: '#94A3B8', padding: 16 } } }
372
+ }
373
+ });
374
+
375
+ // 3. Activity Bar – last 7 days
376
+ const days = [];
377
+ const dayCounts = [];
378
+ for (let i = 6; i >= 0; i--) {
379
+ const d = new Date(); d.setDate(d.getDate() - i);
380
+ days.push(d.toLocaleDateString([], { weekday: 'short' }));
381
+ dayCounts.push(a.filter(x => new Date(x.date).toDateString() === d.toDateString()).length);
382
+ }
383
+ const actCtx = $('activityChart').getContext('2d');
384
+ if (activityInst) activityInst.destroy();
385
+ activityInst = new Chart(actCtx, {
386
+ type: 'bar',
387
+ data: {
388
+ labels: days,
389
+ datasets: [
390
+ { label: 'Analyses', data: dayCounts, backgroundColor: 'rgba(20,184,166,0.75)', borderRadius: 8, borderSkipped: false }
391
+ ]
392
+ },
393
+ options: {
394
+ responsive: true, maintainAspectRatio: false,
395
+ plugins: { legend: { labels: { color: '#94A3B8' } } },
396
+ scales: {
397
+ x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(255,255,255,.04)' } },
398
+ y: { ticks: { color: '#64748B', stepSize: 1 }, grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true }
399
+ }
400
+ }
401
+ });
402
+
403
+ // 4. System Stats
404
+ const avg = a.length ? Math.round(a.reduce((s, x) => s + (x.confidence || 0), 0) / a.length) : 0;
405
+ const sysGrid = $('sysStatsGrid');
406
+ sysGrid.innerHTML = `
407
+ <div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--blue-light)">${u.length}</div><div class="sys-stat-lbl">Registered Users</div></div>
408
+ <div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--teal)">${a.length}</div><div class="sys-stat-lbl">Total Analyses</div></div>
409
+ <div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--green)">${avg}%</div><div class="sys-stat-lbl">Avg Confidence</div></div>
410
+ <div class="sys-stat-item"><div class="sys-stat-val" style="color:var(--orange)">${stutter}</div><div class="sys-stat-lbl">Stutter Detections</div></div>
411
+ `;
412
+
413
+ chartsRendered = true;
414
+ }
415
+
416
+ function getLast6Months() {
417
+ const result = [];
418
+ for (let i = 5; i >= 0; i--) {
419
+ const d = new Date();
420
+ d.setMonth(d.getMonth() - i);
421
+ result.push({
422
+ year: d.getFullYear(), month: d.getMonth(),
423
+ label: d.toLocaleDateString([], { month: 'short', year: '2-digit' })
424
+ });
425
+ }
426
+ return result;
427
+ }
428
+
429
+ /* ── Escape HTML ─────────────────────────────────────────── */
430
+ function esc(s) {
431
+ return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
432
+ }
433
+
434
+ /* ── Init ────────────────────────────────────────────────── */
435
+ renderDashboard();
436
+ renderUsersTable(users());
437
+ renderAnalysesTable(analyses());