krsyer-server-monitor-pro 1.0.31 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1180 @@
1
+ const API_URL = '/api/server/info';
2
+
3
+ // --- Charts Configuration ---
4
+ let cpuChart, memChart, diskChart;
5
+ const CHART_POINTS = 30;
6
+
7
+ function initCharts() {
8
+ const commonOptions = {
9
+ responsive: true,
10
+ maintainAspectRatio: false,
11
+ animation: { duration: 0 },
12
+ scales: {
13
+ x: { display: false },
14
+ y: {
15
+ min: 0,
16
+ max: 100,
17
+ ticks: { color: '#94a3b8', font: { size: 10 } },
18
+ grid: { color: 'rgba(255, 255, 255, 0.05)' }
19
+ }
20
+ },
21
+ plugins: { legend: { display: false } },
22
+ elements: {
23
+ point: { radius: 0 },
24
+ line: { tension: 0.4, borderWidth: 2 }
25
+ }
26
+ };
27
+
28
+ const createChart = (id, color, bgColor) => {
29
+ const ctx = document.getElementById(id);
30
+ if (!ctx) return null;
31
+ return new Chart(ctx, {
32
+ type: 'line',
33
+ data: {
34
+ labels: Array(CHART_POINTS).fill(''),
35
+ datasets: [{
36
+ data: Array(CHART_POINTS).fill(0),
37
+ borderColor: color,
38
+ backgroundColor: bgColor,
39
+ fill: true,
40
+ borderWidth: 2
41
+ }]
42
+ },
43
+ options: commonOptions
44
+ });
45
+ };
46
+
47
+ if (!cpuChart) cpuChart = createChart('cpu-chart', '#8b5cf6', 'rgba(139, 92, 246, 0.15)');
48
+ if (!memChart) memChart = createChart('mem-chart', '#06b6d4', 'rgba(6, 182, 212, 0.15)');
49
+ if (!diskChart) diskChart = createChart('disk-chart', '#f43f5e', 'rgba(244, 63, 94, 0.15)');
50
+ }
51
+
52
+ function updateChartData(chart, value) {
53
+ if (!chart) return;
54
+ const data = chart.data.datasets[0].data;
55
+ data.push(value);
56
+ if (data.length > CHART_POINTS) data.shift();
57
+ chart.update('none'); // mode 'none' for performance
58
+ }
59
+
60
+
61
+ // Initial Load & Event Listeners
62
+ document.addEventListener('DOMContentLoaded', () => {
63
+ initCharts(); // Initialize Charts
64
+ fetchServerData();
65
+ // Double fetch to ensure data is populated
66
+ setTimeout(fetchServerData, 500);
67
+ checkCloudflareStatus();
68
+
69
+ // Refresh every 10 seconds
70
+ setInterval(fetchServerData, 10000);
71
+
72
+ // Initial Docker fetch
73
+ setTimeout(fetchDockerData, 1000);
74
+
75
+ // Navigation Logic
76
+ const navLinks = document.querySelectorAll('.nav-link');
77
+
78
+ navLinks.forEach(link => {
79
+ link.addEventListener('click', (e) => {
80
+ e.preventDefault();
81
+
82
+ // Update Active State
83
+ navLinks.forEach(l => l.classList.remove('active'));
84
+ link.classList.add('active');
85
+
86
+ // Show View
87
+ const viewId = link.getAttribute('data-target');
88
+ showView(viewId);
89
+ });
90
+ });
91
+
92
+ // Default View
93
+ showView('view-server');
94
+ });
95
+
96
+ // Navigation Helper
97
+ function showView(viewId) {
98
+ // Hide all views
99
+ document.querySelectorAll('.view-section').forEach(el => {
100
+ el.classList.remove('active');
101
+ });
102
+
103
+ // Show target view
104
+ const target = document.getElementById(viewId);
105
+ if (target) {
106
+ target.classList.add('active');
107
+
108
+ // Update Title
109
+ const titleMap = {
110
+ 'view-server': 'System Infrastructure',
111
+ 'view-network': 'Network Topology',
112
+ 'view-cloudflare': 'Cloudflare Guard',
113
+ 'view-databases': 'Database Clusters',
114
+ 'view-docker': 'Container Environment',
115
+ 'view-apps': 'Application Stack',
116
+ 'view-terminal': 'Web Terminal'
117
+ };
118
+ const titleEl = document.getElementById('page-title');
119
+ if (titleEl) titleEl.textContent = titleMap[viewId] || 'Dashboard';
120
+ }
121
+
122
+ // Initialize Terminal if selected
123
+ if (viewId === 'view-terminal') {
124
+ initTerminal();
125
+ }
126
+ }
127
+
128
+ // Data Fetching
129
+ async function fetchServerData() {
130
+ try {
131
+ const response = await fetch(API_URL);
132
+ const data = await response.json();
133
+
134
+ if (data.status === 'success') {
135
+ updateDashboard(data);
136
+ if (data.subscription) {
137
+ const headerInfo = document.getElementById('license-info-header');
138
+ const planBadge = document.getElementById('license-plan-badge');
139
+ const expiryDisplay = document.getElementById('license-expiry-display');
140
+
141
+ if (headerInfo && data.subscription.active) {
142
+ headerInfo.style.display = 'block';
143
+ const expiryText = data.subscription.expiry ? new Date(data.subscription.expiry).toLocaleDateString() : 'Lifetime';
144
+
145
+ if (planBadge) planBadge.textContent = data.subscription.plan;
146
+ if (expiryDisplay) expiryDisplay.textContent = 'Valid until: ' + expiryText;
147
+
148
+ // Update Footer as well
149
+ const footerPlan = document.getElementById('license-plan');
150
+ const footerExpiry = document.getElementById('license-expiry');
151
+ if (footerPlan) footerPlan.textContent = data.subscription.plan + ' Plan';
152
+ if (footerExpiry) footerExpiry.textContent = 'Valid until: ' + expiryText;
153
+ }
154
+ }
155
+ // Ensure these are called regularly
156
+ if (window.fetchDockerData) window.fetchDockerData();
157
+ }
158
+ } catch (error) {
159
+ console.error('Error fetching server data:', error);
160
+ }
161
+ }
162
+
163
+ function updateDashboard(data) {
164
+ const { server, cpu, memory, disk } = data;
165
+
166
+ // OS Details
167
+ if (server.os) {
168
+ safeSetText('os-name', server.os);
169
+ safeSetText('os-kernel', server.kernel);
170
+ safeSetText('os-arch', server.architecture);
171
+ }
172
+
173
+ // Hostname, Platform, Node, Uptime
174
+ safeSetText('server-hostname', server.hostname);
175
+ if (data.node) safeSetText('node-version', data.node.version);
176
+ safeSetText('server-platform', `${server.platform} (${server.architecture})`);
177
+
178
+ // Uptime
179
+ const uptimeSeconds = server.uptime.raw || 0;
180
+ const uptimeHours = (uptimeSeconds / 3600).toFixed(1);
181
+ safeSetText('server-uptime', `${uptimeHours} Hours`);
182
+
183
+ // CPU Usage
184
+ const cpuLoadVal = cpu.loadAverage.percentage || cpu.loadAverage['1min'];
185
+ const cpuLoad = parseFloat(cpuLoadVal) || 0;
186
+ updateChartData(cpuChart, cpuLoad);
187
+
188
+ if (cpu.model) safeSetText('cpu-model', cpu.model);
189
+ if (cpu.cores) safeSetText('cpu-cores', cpu.cores);
190
+
191
+
192
+ // RAM Metrics
193
+ safeSetText('mem-used', `${memory.used} / ${memory.total}`);
194
+ const memPercentVal = parseFloat(memory.usagePercentage) || 0;
195
+ updateChartData(memChart, memPercentVal);
196
+
197
+ // Disk Metrics
198
+ if (disk) {
199
+ safeSetText('disk-drive', disk.drive);
200
+ safeSetText('disk-info', `${disk.used} / ${disk.total}`);
201
+ const diskPercentVal = parseFloat(disk.percent) || 0;
202
+ updateChartData(diskChart, diskPercentVal);
203
+ }
204
+
205
+ // Network Info
206
+ if (data.network) {
207
+ safeSetText('net-public', data.network.publicIP || 'Unavailable');
208
+ safeSetText('net-local', Array.isArray(data.network.localIPs) ? data.network.localIPs.map(ip => ip.address).join(', ') : (data.network.localIPs || '-'));
209
+ }
210
+
211
+ if (data.applications) updateApplications(data.applications);
212
+ if (data.hostServices) updateDatabases(data.hostServices);
213
+ }
214
+
215
+ // Applications List
216
+ // Applications List (PM2)
217
+ function updateApplications(apps) {
218
+ const list = document.getElementById('apps-list');
219
+ if (!list) return;
220
+
221
+ if (!apps || apps.length === 0) {
222
+ list.innerHTML = '<tr><td colspan="6" style="text-align:center; color: var(--text-secondary);">No active applications</td></tr>';
223
+ return;
224
+ }
225
+
226
+ list.innerHTML = apps.map(app => `
227
+ <tr>
228
+ <td style="color: var(--primary); font-weight: 500;">${app.name} <span style="font-size:0.7em; opacity:0.6">(${app.user || 'root'})</span></td>
229
+ <td><span class="status-badge ${app.status === 'online' ? 'status-online' : 'status-offline'}">${app.status}</span></td>
230
+ <td>${app.memory}</td>
231
+ <td>${app.cpu}</td>
232
+ <td>${app.uptime}</td>
233
+ <td>
234
+ <button class="action-btn" onclick="restartApp('${app.name}', '${app.user || ''}')" title="Restart">
235
+ <ion-icon name="refresh-outline"></ion-icon>
236
+ </button>
237
+ </td>
238
+ </tr>
239
+ `).join('');
240
+ }
241
+
242
+ // Database Services & Active Host Services
243
+ function updateDatabases(services) {
244
+ // 1. Database Services
245
+ const list = document.getElementById('database-services-list');
246
+ if (list) {
247
+ const dbKeywords = ['mysql', 'mysqld', 'mariadb', 'postgres', 'mongod', 'redis'];
248
+ const dbServices = services.filter(svc => {
249
+ const cmd = (svc.command || svc.name || '').toLowerCase();
250
+ return dbKeywords.some(k => cmd.includes(k));
251
+ });
252
+
253
+ if (!dbServices || dbServices.length === 0) {
254
+ list.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--text-secondary);">No database services found</td></tr>';
255
+ } else {
256
+ list.innerHTML = dbServices.map(svc => `
257
+ <tr>
258
+ <td>${svc.command || svc.name || 'Unknown'}</td>
259
+ <td><span class="status-badge ${svc.status === 'stopped' ? 'status-offline' : 'status-online'}">${svc.status || 'Active'}</span></td>
260
+ <td>${svc.memory || '-'}</td>
261
+ <td>${svc.cpu || '-'}</td>
262
+ <td>${svc.uptime || '-'}</td>
263
+ <td>${svc.port || svc.address}</td>
264
+ <td>
265
+ ${svc.unit ? `
266
+ <button class="action-btn" style="color:var(--accent-color)" onclick="restartService('${svc.unit}')" title="Restart Service">
267
+ <ion-icon name="refresh-outline"></ion-icon>
268
+ </button>
269
+ ${svc.status === 'stopped' ?
270
+ `<button class="action-btn" style="color:var(--success)" onclick="startService('${svc.unit}')" title="Start Service"><ion-icon name="play-circle-outline"></ion-icon></button>`
271
+ :
272
+ `<button class="action-btn" style="color:var(--danger)" onclick="stopService('${svc.unit}')" title="Stop Service"><ion-icon name="stop-circle-outline"></ion-icon></button>`
273
+ }
274
+ ` : '<span style="color:gray; font-size:0.8rem">N/A</span>'}
275
+ </td>
276
+ </tr>
277
+ `).join('');
278
+ }
279
+ }
280
+
281
+ // 2. Active Host Services (Web Apps)
282
+ const hostList = document.getElementById('host-services-list');
283
+ if (hostList) {
284
+ const filterList = ['apache2', 'httpd', 'nginx', 'node', 'pm2', 'python'];
285
+ const webServices = services.filter(svc => {
286
+ const cmd = (svc.command || svc.name || '').toLowerCase();
287
+ return filterList.some(k => cmd.includes(k)) || !!svc.publicUrl;
288
+ });
289
+
290
+ if (!webServices || webServices.length === 0) {
291
+ hostList.innerHTML = '<tr><td colspan="8" style="text-align:center; color: var(--text-secondary);">No active web services found</td></tr>';
292
+ } else {
293
+ hostList.innerHTML = webServices.map(svc => {
294
+ // Parse Port logic
295
+ let port = '-';
296
+ if (svc.address) {
297
+ const lastColon = svc.address.lastIndexOf(':');
298
+ if (lastColon !== -1) {
299
+ port = svc.address.substring(lastColon + 1);
300
+ }
301
+ }
302
+
303
+ // Domain Logic
304
+ let domainDisplay = '-';
305
+ if (svc.publicUrl) {
306
+ domainDisplay = svc.publicUrl.replace(/^https?:\/\//, '').replace(/\/$/, '').toLowerCase();
307
+ }
308
+
309
+ // Construct URL
310
+ let openUrl = svc.publicUrl;
311
+ if (!openUrl && port !== '-' && port !== '*') {
312
+ openUrl = `${window.location.protocol}//${window.location.hostname}:${port}`;
313
+ }
314
+
315
+ return `
316
+ <tr>
317
+ <td>${svc.command || svc.name || 'Unknown'}</td>
318
+ <td><span class="status-badge ${svc.status === 'stopped' ? 'status-offline' : 'status-online'}">${svc.status || 'Active'}</span></td>
319
+ <td>${svc.memory || '-'}</td>
320
+ <td>${svc.cpu || '-'}</td>
321
+ <td style="font-family:monospace; color: #a78bfa;">${domainDisplay}</td>
322
+ <td style="font-family:monospace; color: var(--warning);">${port}</td>
323
+ <td>
324
+ ${openUrl ?
325
+ `<a href="${openUrl}" target="_blank" class="action-btn connect-btn" style="text-decoration:none; padding:4px 10px; font-size: 0.8rem;">
326
+ <ion-icon name="open-outline" style="vertical-align:middle"></ion-icon> Open
327
+ </a>` : '-'}
328
+ </td>
329
+ <td>
330
+ ${svc.unit ? `
331
+ <button class="action-btn" style="color:var(--accent-color)" onclick="restartService('${svc.unit}')" title="Restart Service">
332
+ <ion-icon name="refresh-outline"></ion-icon>
333
+ </button>
334
+ <button class="action-btn" style="color:var(--danger)" onclick="stopService('${svc.unit}')" title="Stop Service">
335
+ <ion-icon name="stop-circle-outline"></ion-icon>
336
+ </button>
337
+ ` : '<span style="color:gray; font-size:0.8rem">N/A</span>'}
338
+ </td>
339
+ </tr>
340
+ `}).join('');
341
+ }
342
+ }
343
+ }
344
+
345
+ // Network List
346
+ async function scanNetwork() {
347
+ const list = document.getElementById('network-list');
348
+ const btn = document.getElementById('scan-btn');
349
+
350
+ list.innerHTML = '<div style="text-align:center; padding: 1rem; color: var(--text-secondary);"><ion-icon name="sync-outline" class="spin" style="font-size: 1.5rem;"></ion-icon><br>Scanning Subnets...</div>';
351
+ if (btn) {
352
+ btn.disabled = true;
353
+ btn.innerHTML = '<ion-icon name="sync-outline" class="spin"></ion-icon> Scanning';
354
+ }
355
+
356
+ try {
357
+ const response = await fetch('/api/network/scan');
358
+ const data = await response.json();
359
+
360
+ if (data.status === 'success') {
361
+ updateNetworkList(data.servers, data.subnets);
362
+ } else {
363
+ list.innerHTML = `<div style="text-align:center; color: var(--danger);">Scan failed: ${data.message}</div>`;
364
+ }
365
+ } catch (error) {
366
+ console.error('Network scan error:', error);
367
+ list.innerHTML = `<div style="text-align:center; color: var(--danger);">Network Error. Check console.</div>`;
368
+ } finally {
369
+ if (btn) {
370
+ btn.disabled = false;
371
+ btn.innerHTML = '<ion-icon name="scan-outline"></ion-icon> Scan';
372
+ }
373
+ }
374
+ }
375
+
376
+ function updateNetworkList(servers, subnets) {
377
+ const list = document.getElementById('network-list');
378
+ if (!list) return;
379
+
380
+ const subnetDisplay = Array.isArray(subnets) ? subnets.join(', ') : subnets;
381
+
382
+ if (!servers || servers.length === 0) {
383
+ list.innerHTML = `<div style="text-align:center; color: var(--text-secondary);">No servers found in ${subnetDisplay}</div>`;
384
+ return;
385
+ }
386
+
387
+ list.innerHTML = servers.map(server => {
388
+ const isCurrent = server.status === 'current';
389
+ let link = server.publicUrl || `http://${server.ip}:3010`;
390
+ // Legacy overrides
391
+ if (!server.publicUrl) {
392
+ if (server.ip === '192.168.1.8') link = 'http://monitors1.mulasofttechnologies.com';
393
+ else if (server.ip === '192.168.1.6') link = 'http://monitor.mulasofttechnologies.com';
394
+ }
395
+
396
+ return `
397
+ <div class="network-item">
398
+ <div class="network-info">
399
+ <ion-icon name="server-outline" style="color: ${isCurrent ? 'var(--accent-color)' : 'var(--success)'}"></ion-icon>
400
+ <div>
401
+ <div class="server-ip">${server.ip}</div>
402
+ <div class="server-status">${isCurrent ? 'Current Server' : 'Online'}</div>
403
+ </div>
404
+ </div>
405
+ ${!isCurrent ? `
406
+ <a href="${link}" target="_blank" class="connect-btn">
407
+ View <ion-icon name="open-outline"></ion-icon>
408
+ </a>` : ''}
409
+ </div>
410
+ `;
411
+ }).join('');
412
+ }
413
+
414
+
415
+ // Actions
416
+ async function restartApp(id, user) {
417
+ if (!confirm(`Restart application ${id}?`)) return;
418
+ try {
419
+ const body = user ? JSON.stringify({ user }) : null;
420
+ const res = await fetch(`/api/server/restart/${id}`, {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: body
424
+ });
425
+ const data = await res.json();
426
+ if (res.ok) {
427
+ alert(data.message);
428
+ fetchServerData();
429
+ } else {
430
+ alert('Error: ' + data.message);
431
+ }
432
+ } catch (e) {
433
+ alert('Action failed');
434
+ }
435
+ }
436
+
437
+ async function restartService(name) {
438
+ if (!confirm(`Restart system service ${name}?`)) return;
439
+ try {
440
+ const res = await fetch(`/api/server/service/restart/${name}`, { method: 'POST' });
441
+ const data = await res.json();
442
+ if (res.ok) {
443
+ alert(data.message);
444
+ fetchServerData();
445
+ } else {
446
+ alert('Error: ' + data.message);
447
+ }
448
+ } catch (e) {
449
+ alert('Action failed');
450
+ }
451
+ }
452
+
453
+ async function stopService(name) {
454
+ if (!confirm(`Stop system service ${name}? WARNING: This may disrupt operations.`)) return;
455
+ try {
456
+ const res = await fetch(`/api/server/service/stop/${name}`, { method: 'POST' });
457
+ const data = await res.json();
458
+ if (res.ok) {
459
+ alert(data.message);
460
+ fetchServerData();
461
+ } else {
462
+ alert('Error: ' + data.message);
463
+ }
464
+ } catch (e) {
465
+ alert('Action failed');
466
+ }
467
+ }
468
+
469
+ async function startService(name) {
470
+ if (!confirm(`Start system service ${name}?`)) return;
471
+ try {
472
+ const res = await fetch(`/api/server/service/start/${name}`, { method: 'POST' });
473
+ const data = await res.json();
474
+ if (res.ok) {
475
+ alert(data.message);
476
+ fetchServerData();
477
+ } else {
478
+ alert('Error: ' + data.message);
479
+ }
480
+ } catch (e) {
481
+ alert('Action failed');
482
+ }
483
+ }
484
+
485
+
486
+ // Docker Functions
487
+ async function fetchDockerData() {
488
+ try {
489
+ const response = await fetch('/api/docker/containers');
490
+ if (response.status === 401) return;
491
+ const data = await response.json();
492
+ if (data.status === 'success') {
493
+ updateDockerTable(data.containers);
494
+ }
495
+ } catch (e) {
496
+ console.error('Docker fetch error', e);
497
+ }
498
+
499
+ // Also fetch Docker images
500
+ fetchDockerImages();
501
+ }
502
+
503
+ async function fetchDockerImages() {
504
+ try {
505
+ const response = await fetch('/api/docker/images');
506
+ if (response.status === 401) return;
507
+ const data = await response.json();
508
+ if (data.status === 'success') {
509
+ updateDockerImagesTable(data.images);
510
+ }
511
+ } catch (e) {
512
+ console.error('Docker images fetch error', e);
513
+ }
514
+ }
515
+
516
+ function updateDockerImagesTable(images) {
517
+ const tbody = document.getElementById('docker-images-list');
518
+ if (!tbody) return;
519
+
520
+ if (!images || images.length === 0) {
521
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color: var(--text-secondary);">No images found</td></tr>';
522
+ return;
523
+ }
524
+
525
+ tbody.innerHTML = images.map(img => {
526
+ // Parse size to show in human-readable format
527
+ // Display Size directly (already formatted by Docker, e.g., "1.2GB")
528
+ const sizeDisplay = img.Size || '-';
529
+
530
+ return `
531
+ <tr>
532
+ <td data-label="Repository">${img.Repository || 'none'}</td>
533
+ <td data-label="Tag" style="font-size:0.85rem;">${img.Tag || 'none'}</td>
534
+ <td data-label="Image ID" style="font-size:0.85rem; color:var(--text-secondary);">${img.ID ? img.ID.substring(0, 12) : '-'}</td>
535
+ <td data-label="Size">${sizeDisplay}</td>
536
+ <td data-label="Actions">
537
+ <div style="display:flex; gap: 0.5rem; justify-content: flex-end;">
538
+ <button class="action-btn" style="color:var(--danger)" onclick="dockerImageAction('remove', '${img.ID}')" title="Remove Image">
539
+ <ion-icon name="trash-outline"></ion-icon>
540
+ </button>
541
+ </div>
542
+ </td>
543
+ </tr>`;
544
+ }).join('');
545
+ }
546
+
547
+ async function dockerImageAction(action, id) {
548
+ if (action === 'remove') {
549
+ if (!confirm('Are you sure you want to remove this image?')) return;
550
+
551
+ try {
552
+ const res = await fetch(`/api/docker/image/${id}`, { method: 'DELETE' });
553
+ const data = await res.json();
554
+ if (res.ok) {
555
+ alert(data.message);
556
+ fetchDockerImages();
557
+ } else {
558
+ alert('Error: ' + data.message);
559
+ }
560
+ } catch (e) {
561
+ alert('Action failed');
562
+ }
563
+ }
564
+ }
565
+
566
+
567
+ function updateDockerTable(containers) {
568
+ const tbody = document.getElementById('docker-list');
569
+ if (!tbody) return;
570
+
571
+ if (!containers || containers.length === 0) {
572
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; color: var(--text-secondary);">No active containers</td></tr>';
573
+ return;
574
+ }
575
+
576
+ tbody.innerHTML = containers.map(c => {
577
+ const isRunning = c.State && (c.State === 'running' || c.Status.startsWith('Up'));
578
+ const statusClass = isRunning ? 'status-online' : 'status-offline';
579
+
580
+ return `
581
+ <tr>
582
+ <td data-label="Names">${c.Names}</td>
583
+ <td data-label="Image" style="font-size:0.85rem; color:var(--text-secondary);">${c.Image}</td>
584
+ <td data-label="Status"><span class="status-badge ${statusClass}">${c.Status}</span></td>
585
+ <td data-label="Ports" style="font-size:0.85rem;">${c.Ports || '-'}</td>
586
+ <td data-label="Actions">
587
+ <div style="display:flex; gap: 0.5rem; justify-content: flex-end;">
588
+ ${isRunning ?
589
+ `<button class="action-btn" style="color:var(--warning)" onclick="dockerAction('stop', '${c.ID}')" title="Stop"><ion-icon name="stop-circle-outline"></ion-icon></button>
590
+ <button class="action-btn" style="color:var(--accent-color)" onclick="dockerAction('restart', '${c.ID}')" title="Restart"><ion-icon name="refresh-outline"></ion-icon></button>`
591
+ :
592
+ `<button class="action-btn" style="color:var(--success)" onclick="dockerAction('start', '${c.ID}')" title="Start"><ion-icon name="play-circle-outline"></ion-icon></button>`
593
+ }
594
+ <button class="action-btn" style="color:var(--danger)" onclick="dockerAction('remove', '${c.ID}')" title="Remove"><ion-icon name="trash-outline"></ion-icon></button>
595
+ </div>
596
+ </td>
597
+ </tr>`;
598
+ }).join('');
599
+ }
600
+
601
+ async function dockerAction(action, id) {
602
+ if (!confirm(`Are you sure you want to ${action} this container?`)) return;
603
+
604
+ const method = action === 'remove' ? 'DELETE' : 'POST';
605
+ const url = action === 'remove' ? `/api/docker/${id}` : `/api/docker/${action}/${id}`;
606
+
607
+ try {
608
+ const res = await fetch(url, { method });
609
+ const data = await res.json();
610
+ if (res.ok) {
611
+ alert(data.message);
612
+ fetchDockerData();
613
+ } else {
614
+ alert('Error: ' + data.message);
615
+ }
616
+ } catch (e) {
617
+ alert('Action failed');
618
+ }
619
+ }
620
+
621
+ function showCreateContainer() {
622
+ const el = document.getElementById('create-container-form');
623
+ if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
624
+ }
625
+
626
+ async function createContainer() {
627
+ const image = document.getElementById('docker-image').value;
628
+ const name = document.getElementById('docker-name').value;
629
+ const ports = document.getElementById('docker-ports').value;
630
+ const restart = document.getElementById('docker-restart').value;
631
+ const network = document.getElementById('docker-network').value;
632
+ const envRaw = document.getElementById('docker-env').value;
633
+ const volumesRaw = document.getElementById('docker-volumes').value;
634
+
635
+ const logDiv = document.getElementById('docker-creation-logs');
636
+ const btn = document.querySelector('#create-container-form button.connect-btn');
637
+
638
+ if (!image) { alert('Image is required'); return; }
639
+
640
+ const env = envRaw.split('\n').map(l => l.trim()).filter(l => l);
641
+ const volumes = volumesRaw.split('\n').map(l => l.trim()).filter(l => l);
642
+
643
+ if (logDiv) {
644
+ logDiv.style.display = 'block';
645
+ logDiv.innerHTML = `> Initializing deployment for image: ${image}...\n`;
646
+ logDiv.innerHTML += `> Contacting Docker daemon...\n`;
647
+ }
648
+
649
+ const originalBtnText = btn.innerHTML;
650
+ btn.disabled = true;
651
+ btn.innerHTML = '<ion-icon name="sync-outline" class="spin"></ion-icon> Deploying...';
652
+
653
+ try {
654
+ const res = await fetch('/api/docker/run', {
655
+ method: 'POST',
656
+ headers: { 'Content-Type': 'application/json' },
657
+ body: JSON.stringify({ image, name, ports, restart, network, env, volumes })
658
+ });
659
+ const data = await res.json();
660
+
661
+ if (res.ok) {
662
+ if (logDiv) {
663
+ logDiv.style.display = 'none';
664
+ logDiv.innerHTML = '';
665
+ }
666
+ fetchDockerData();
667
+
668
+ // Clear inputs
669
+ ['docker-image', 'docker-name', 'docker-ports', 'docker-env', 'docker-volumes'].forEach(id => {
670
+ const el = document.getElementById(id);
671
+ if (el) el.value = '';
672
+ });
673
+
674
+ alert(`Task Completed: Docker Container Created Successfully.\n\nID: ${data.logs ? data.logs.substring(0, 12) : 'N/A'}`);
675
+ document.getElementById('create-container-form').style.display = 'none';
676
+
677
+ } else {
678
+ if (logDiv) logDiv.innerHTML += `> ERROR: ${data.message}\n`;
679
+ }
680
+ } catch (e) {
681
+ if (logDiv) logDiv.innerHTML += `> FATAL ERROR: ${e.message}\n`;
682
+ } finally {
683
+ btn.disabled = false;
684
+ btn.innerHTML = originalBtnText;
685
+ }
686
+ }
687
+
688
+
689
+ // Cloudflare Logic
690
+ async function addCloudflareProxy() {
691
+ const tunnelName = document.getElementById('cf-tunnel').value;
692
+ const hostname = document.getElementById('cf-hostname').value;
693
+ const port = document.getElementById('cf-port').value;
694
+ const ip = document.getElementById('cf-ip').value;
695
+ let configFilename = document.getElementById('cf-config').value;
696
+
697
+ // Use dropdown value if config input is empty
698
+ const dropdown = document.getElementById('cf-files-dropdown');
699
+ if (!configFilename && dropdown && dropdown.value) {
700
+ configFilename = dropdown.value;
701
+ }
702
+
703
+ if (!tunnelName || !hostname || !port) {
704
+ alert('Please fill Tunnel Name, Hostname, and Port');
705
+ return;
706
+ }
707
+
708
+ const btn = document.querySelector('#cf-installed-content .connect-btn');
709
+ const logs = document.getElementById('cf-logs');
710
+ if (btn) {
711
+ btn.disabled = true;
712
+ btn.innerText = 'Adding Route...';
713
+ }
714
+ if (logs) {
715
+ logs.style.display = 'block';
716
+ logs.innerHTML = '<div>> Initiating...</div>';
717
+ }
718
+
719
+ try {
720
+ const res = await fetch('/api/cloudflare/add', {
721
+ method: 'POST',
722
+ headers: { 'Content-Type': 'application/json' },
723
+ body: JSON.stringify({ tunnelName, hostname, port, serverIp: ip, configFilename })
724
+ });
725
+
726
+ const contentType = res.headers.get("content-type");
727
+ if (contentType && contentType.indexOf("application/json") !== -1) {
728
+ const data = await res.json();
729
+ if (res.ok) {
730
+ if (logs) {
731
+ logs.style.display = 'none';
732
+ logs.innerHTML = '';
733
+ }
734
+ const hostInput = document.getElementById('cf-hostname');
735
+ const portInput = document.getElementById('cf-port');
736
+ if (hostInput) hostInput.value = '';
737
+ if (portInput) portInput.value = '';
738
+
739
+ alert(`Task Completed: ${data.message}`);
740
+ fetchCloudflareRoutes(tunnelName, configFilename);
741
+ } else {
742
+ if (logs) logs.innerHTML += `> ERROR: ${data.message}\n`;
743
+ }
744
+ } else {
745
+ const text = await res.text();
746
+ if (logs) logs.innerHTML += `> UNEXPECTED RESPONSE: ${text.substring(0, 200)}...\n`;
747
+ }
748
+
749
+ } catch (e) {
750
+ if (logs) logs.innerHTML += `> FATAL ERROR: ${e.message}\n`;
751
+ } finally {
752
+ if (btn) {
753
+ btn.disabled = false;
754
+ btn.innerHTML = 'Add & Restart';
755
+ }
756
+ }
757
+ }
758
+
759
+ async function checkCloudflareStatus() {
760
+ try {
761
+ const res = await fetch('/api/cloudflare/status');
762
+ const data = await res.json();
763
+
764
+ const notInstalledDiv = document.getElementById('cf-not-installed');
765
+ const installedDiv = document.getElementById('cf-installed-content');
766
+
767
+ if (data.status === 'success' && data.installed) {
768
+ if (notInstalledDiv) notInstalledDiv.style.display = 'none';
769
+ if (installedDiv) installedDiv.style.display = 'block';
770
+ fetchCloudflareFiles();
771
+ } else {
772
+ if (notInstalledDiv) notInstalledDiv.style.display = 'block';
773
+ if (installedDiv) installedDiv.style.display = 'none';
774
+ }
775
+ } catch (e) {
776
+ console.error('Status check failed', e);
777
+ }
778
+ }
779
+
780
+ async function fetchCloudflareFiles() {
781
+ const listDropdown = document.getElementById('cf-files-dropdown');
782
+ const tunnelDropdown = document.getElementById('cf-tunnel');
783
+
784
+ // 1. Files for Active Routes List
785
+ if (listDropdown) {
786
+ try {
787
+ const res = await fetch('/api/cloudflare/files');
788
+ const data = await res.json();
789
+ if (data.status === 'success' && data.files.length > 0) {
790
+ listDropdown.innerHTML = data.files.map(f => `<option value="${f}">${f}</option>`).join('');
791
+ const defaultFile = data.files.find(f => f.includes('krsyergroup')) || data.files[0];
792
+ listDropdown.value = defaultFile;
793
+ fetchCloudflareRoutes();
794
+ } else {
795
+ listDropdown.innerHTML = '<option disabled>No files</option>';
796
+ }
797
+ } catch (e) {
798
+ console.error('Failed to load CF files', e);
799
+ listDropdown.innerHTML = '<option error>Error</option>';
800
+ }
801
+ }
802
+
803
+ // 2. Tunnels for Add Route Dropdown (CLI Source)
804
+ if (tunnelDropdown) {
805
+ try {
806
+ const res = await fetch('/api/cloudflare/tunnels');
807
+ const data = await res.json();
808
+ if (data.status === 'success' && data.tunnels.length > 0) {
809
+ tunnelDropdown.innerHTML = data.tunnels.map(name => `<option value="${name}">${name}</option>`).join('');
810
+ const defaultTunnel = data.tunnels.find(t => t.includes('krsyergroup')) || data.tunnels[0];
811
+ tunnelDropdown.value = defaultTunnel;
812
+ } else {
813
+ tunnelDropdown.innerHTML = '<option disabled>No active tunnels</option>';
814
+ }
815
+ } catch (e) {
816
+ console.error('Failed to load CF tunnels', e);
817
+ tunnelDropdown.innerHTML = '<option error>Error</option>';
818
+ }
819
+ }
820
+ }
821
+ // Add event listener for dropdown change
822
+ document.getElementById('cf-files-dropdown')?.addEventListener('change', () => {
823
+ fetchCloudflareRoutes();
824
+ });
825
+
826
+
827
+ async function fetchCloudflareRoutes(tunnelName, configFile) {
828
+ const dropdown = document.getElementById('cf-files-dropdown');
829
+
830
+ if (typeof tunnelName !== 'string') {
831
+ tunnelName = document.getElementById('cf-tunnel').value || 'krsyergroup';
832
+ if (dropdown && dropdown.value) {
833
+ configFile = dropdown.value;
834
+ } else {
835
+ configFile = document.getElementById('cf-config').value;
836
+ }
837
+ }
838
+
839
+ if (!configFile) configFile = `${tunnelName}.yml`;
840
+ if (!configFile.endsWith('.yml')) configFile += '.yml';
841
+
842
+ const configInput = document.getElementById('cf-config');
843
+ if (configInput && configFile) configInput.value = configFile;
844
+
845
+ const listDiv = document.getElementById('cf-routes-list');
846
+ listDiv.innerHTML = '<div style="text-align:center; color: var(--text-secondary);">Loading routes...</div>';
847
+
848
+ try {
849
+ const res = await fetch(`/api/cloudflare/config?tunnel=${tunnelName}&file=${configFile}`);
850
+ const data = await res.json();
851
+
852
+ if (data.status === 'success' && data.config.ingress) {
853
+ const rules = data.config.ingress.filter(r => r.hostname);
854
+
855
+ if (rules.length === 0) {
856
+ listDiv.innerHTML = '<div style="text-align:center; color: var(--text-secondary);">No active routes found.</div>';
857
+ return;
858
+ }
859
+
860
+ listDiv.innerHTML = rules.map(r => `
861
+ <div class="network-item">
862
+ <div class="network-info">
863
+ <ion-icon name="globe" style="color: var(--primary); font-size: 1.5rem;"></ion-icon>
864
+ <div>
865
+ <div class="host-text">${r.hostname}</div>
866
+ <div class="service-text">${r.service}</div>
867
+ </div>
868
+ </div>
869
+ <div class="mobile-actions" style="display:flex; gap:0.75rem;">
870
+ <a href="https://${r.hostname}" target="_blank" class="action-btn" title="Open Site" style="color: var(--success)">
871
+ <ion-icon name="open-outline"></ion-icon>
872
+ </a>
873
+ <button class="action-btn" style="color:var(--primary);" onclick="editCloudflareRoute('${r.hostname}', '${r.service}', '${tunnelName}', '${configFile}')" title="Edit Configuration">
874
+ <ion-icon name="options-outline"></ion-icon>
875
+ </button>
876
+ <button class="action-btn" style="color:var(--danger);" onclick="detachCloudflareRoute('${r.hostname}')" title="Delete Route">
877
+ <ion-icon name="trash-outline"></ion-icon>
878
+ </button>
879
+ </div>
880
+ </div>
881
+ `).join('');
882
+ } else {
883
+ listDiv.innerHTML = `<div style="text-align:center; color: var(--danger);">Failed to load or invalid config (${configFile}).</div>`;
884
+ }
885
+ } catch (e) {
886
+ console.error('CF Config Fetch Error', e);
887
+ listDiv.innerHTML = `<div style="text-align:center; color: var(--danger);">Error loading routes. Check inputs.</div>`;
888
+ }
889
+ }
890
+
891
+ async function detachCloudflareRoute(hostname) {
892
+ if (!confirm(`Are you sure you want to detach ${hostname}?`)) return;
893
+
894
+ const tunnelName = document.getElementById('cf-tunnel').value || 'krsyergroup';
895
+ let configFile = document.getElementById('cf-config').value;
896
+
897
+ const listDiv = document.getElementById('cf-routes-list');
898
+ const originalHTML = listDiv.innerHTML;
899
+ listDiv.innerHTML = '<div style="text-align:center; color: var(--text-secondary);"><ion-icon name="sync-outline" class="spin"></ion-icon> Detaching...</div>';
900
+
901
+ try {
902
+ const res = await fetch('/api/cloudflare/delete', {
903
+ method: 'POST',
904
+ headers: { 'Content-Type': 'application/json' },
905
+ body: JSON.stringify({ hostname, tunnelName, configFilename: configFile })
906
+ });
907
+
908
+ const data = await res.json();
909
+ if (res.ok && data.status === 'success') {
910
+ fetchCloudflareRoutes(tunnelName, configFile);
911
+ } else {
912
+ alert('Error: ' + data.message);
913
+ listDiv.innerHTML = originalHTML;
914
+ }
915
+ } catch (e) {
916
+ alert('Action failed: ' + e.message);
917
+ listDiv.innerHTML = originalHTML;
918
+ }
919
+ }
920
+
921
+ function editCloudflareRoute(hostname, service, tunnelName, configFilename) {
922
+ let ip = '';
923
+ let port = '';
924
+
925
+ const match = service.match(/https?:\/\/([^:]+):(\d+)/);
926
+ if (match) {
927
+ ip = match[1];
928
+ port = match[2];
929
+ } else {
930
+ const matchSimple = service.match(/https?:\/\/([^:]+)/);
931
+ if (matchSimple) ip = matchSimple[1];
932
+ }
933
+
934
+ // Populate Fields
935
+ setInputValue('cf-hostname', hostname);
936
+ setInputValue('cf-port', port);
937
+ setInputValue('cf-ip', ip);
938
+ setInputValue('cf-tunnel', tunnelName);
939
+ setInputValue('cf-config', configFilename);
940
+
941
+ const btn = document.querySelector('#cf-installed-content .connect-btn');
942
+ if (btn) {
943
+ btn.innerHTML = 'Update & Restart';
944
+ btn.classList.add('pulse');
945
+ setTimeout(() => btn.classList.remove('pulse'), 1000);
946
+ }
947
+ document.getElementById('cf-hostname').focus();
948
+ }
949
+
950
+ // Helper to set text content safely
951
+ function safeSetText(id, text) {
952
+ const el = document.getElementById(id);
953
+ if (el) el.textContent = text || '-';
954
+ }
955
+ function setInputValue(id, val) {
956
+ const el = document.getElementById(id);
957
+ if (el) el.value = val || '';
958
+ }
959
+
960
+ // Terminal Logic
961
+ let term;
962
+ let socket;
963
+ let fitAddon;
964
+
965
+ function initTerminal() {
966
+ if (term) {
967
+ fitAddon.fit();
968
+ term.focus();
969
+ return;
970
+ }
971
+
972
+ const container = document.getElementById('terminal-container');
973
+ if (!container) return;
974
+
975
+ term = new Terminal({
976
+ cursorBlink: true,
977
+ fontFamily: 'Consolas, "Courier New", monospace',
978
+ fontSize: 14,
979
+ theme: {
980
+ background: '#0e0e0e',
981
+ foreground: '#f0f0f0',
982
+ cursor: '#4f46e5'
983
+ },
984
+ convertEol: true
985
+ });
986
+
987
+ fitAddon = new FitAddon.FitAddon();
988
+ term.loadAddon(fitAddon);
989
+ term.open(container);
990
+ fitAddon.fit();
991
+
992
+ socket = io();
993
+
994
+ socket.on('connect_error', (err) => {
995
+ term.write(`\r\n\x1b[31mConnection error: ${err.message}\x1b[0m\r\n`);
996
+ });
997
+
998
+ socket.on('terminal:data', (data) => term.write(data));
999
+ term.onData((data) => socket.emit('terminal:write', data));
1000
+
1001
+ window.addEventListener('resize', () => {
1002
+ try {
1003
+ fitAddon.fit();
1004
+ if (socket) socket.emit('terminal:resize', { cols: term.cols, rows: term.rows });
1005
+ } catch (e) {
1006
+ console.error('Resize error:', e);
1007
+ }
1008
+ });
1009
+
1010
+ socket.emit('terminal:resize', { cols: term.cols, rows: term.rows });
1011
+ term.focus();
1012
+ }
1013
+
1014
+ // Search/Filter Functions
1015
+ function filterApps() {
1016
+ const search = document.getElementById('search-apps').value.toLowerCase();
1017
+ const rows = document.querySelectorAll('#apps-list tr');
1018
+ rows.forEach(row => {
1019
+ const text = row.textContent.toLowerCase();
1020
+ row.style.display = text.includes(search) ? '' : 'none';
1021
+ });
1022
+ }
1023
+
1024
+ function filterDatabases() {
1025
+ const search = document.getElementById('search-databases').value.toLowerCase();
1026
+ const rows = document.querySelectorAll('#database-services-list tr');
1027
+ rows.forEach(row => {
1028
+ const text = row.textContent.toLowerCase();
1029
+ row.style.display = text.includes(search) ? '' : 'none';
1030
+ });
1031
+ }
1032
+
1033
+ function filterDocker() {
1034
+ const search = document.getElementById('search-docker').value.toLowerCase();
1035
+ const rows = document.querySelectorAll('#docker-list tr');
1036
+ rows.forEach(row => {
1037
+ const text = row.textContent.toLowerCase();
1038
+ row.style.display = text.includes(search) ? '' : 'none';
1039
+ });
1040
+ }
1041
+
1042
+ function filterDockerImages() {
1043
+ const search = document.getElementById('search-docker-images').value.toLowerCase();
1044
+ const rows = document.querySelectorAll('#docker-images-list tr');
1045
+ rows.forEach(row => {
1046
+ const text = row.textContent.toLowerCase();
1047
+ row.style.display = text.includes(search) ? '' : 'none';
1048
+ });
1049
+ }
1050
+
1051
+ function filterHostServices() {
1052
+ const search = document.getElementById('search-host').value.toLowerCase();
1053
+ const rows = document.querySelectorAll('#host-services-list tr');
1054
+ rows.forEach(row => {
1055
+ const text = row.textContent.toLowerCase();
1056
+ row.style.display = text.includes(search) ? '' : 'none';
1057
+ });
1058
+ }
1059
+
1060
+ // Logout Function
1061
+ function logout() {
1062
+ if (confirm('Are you sure you want to logout?')) {
1063
+ // Clear any stored authentication
1064
+ sessionStorage.clear();
1065
+ localStorage.clear();
1066
+
1067
+ // Redirect to logout or login page
1068
+ window.location.href = '/logout';
1069
+ }
1070
+ }
1071
+
1072
+ // --- Password Management ---
1073
+ function showChangePasswordModal() {
1074
+ let modal = document.getElementById('password-modal');
1075
+ if (!modal) {
1076
+ modal = document.createElement('div');
1077
+ modal.id = 'password-modal';
1078
+ // Glassmorphism Overlay
1079
+ modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); backdrop-filter:blur(5px); display:none; align-items:center; justify-content:center; z-index:9999;';
1080
+
1081
+ // CSS for Spin Animation (Injected once)
1082
+ if (!document.getElementById('spin-style')) {
1083
+ const style = document.createElement('style');
1084
+ style.id = 'spin-style';
1085
+ style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .spin { animation: spin 1s linear infinite; }';
1086
+ document.head.appendChild(style);
1087
+ }
1088
+
1089
+ modal.innerHTML = `
1090
+ <div style="background:#1e293b; padding:2.5rem; border-radius:16px; width:90%; max-width:400px; border:1px solid #334155; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);">
1091
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem; border-bottom:1px solid #334155; padding-bottom:1rem;">
1092
+ <h2 style="margin:0; font-size:1.25rem; color:#fff;">Change Password</h2>
1093
+ <button onclick="closePasswordModal()" style="background:none; border:none; color:#94a3b8; font-size:1.5rem; cursor:pointer;"><ion-icon name="close-outline"></ion-icon></button>
1094
+ </div>
1095
+
1096
+ <div id="pwd-error" style="display:none; background:rgba(239,68,68,0.2); color:#fca5a5; padding:0.75rem; border-radius:8px; margin-bottom:1rem; font-size:0.9rem;"></div>
1097
+
1098
+ <div style="margin-bottom:1rem;">
1099
+ <label style="display:block; color:#cbd5e1; margin-bottom:0.5rem; font-size:0.9rem;">Current Password</label>
1100
+ <input type="password" id="current-pass" class="dark-input" style="width:100%; padding:0.75rem; background:#0f172a; border:1px solid #334155; color:white; border-radius:8px;" placeholder="Current Password">
1101
+ </div>
1102
+
1103
+ <div style="margin-bottom:1.5rem;">
1104
+ <label style="display:block; color:#cbd5e1; margin-bottom:0.5rem; font-size:0.9rem;">New Password</label>
1105
+ <input type="password" id="new-pass" class="dark-input" style="width:100%; padding:0.75rem; background:#0f172a; border:1px solid #334155; color:white; border-radius:8px;" placeholder="New Password">
1106
+ </div>
1107
+
1108
+ <div style="display:flex; justify-content:flex-end; gap:1rem;">
1109
+ <button onclick="closePasswordModal()" style="padding:0.75rem 1.5rem; background:transparent; color:#cbd5e1; border:1px solid #334155; border-radius:8px; cursor:pointer; font-weight:500;">Cancel</button>
1110
+ <button id="update-pass-btn" onclick="submitNewPassword()" style="padding:0.75rem 1.5rem; background:#3b82f6; color:white; border:none; border-radius:8px; cursor:pointer; font-weight:600; display:flex; align-items:center; gap:0.5rem;">
1111
+ Update Password
1112
+ </button>
1113
+ </div>
1114
+ </div>
1115
+ `;
1116
+ document.body.appendChild(modal);
1117
+ }
1118
+
1119
+ // Reset fields
1120
+ document.getElementById('current-pass').value = '';
1121
+ document.getElementById('new-pass').value = '';
1122
+ const err = document.getElementById('pwd-error');
1123
+ if (err) err.style.display = 'none';
1124
+
1125
+ const btn = document.getElementById('update-pass-btn');
1126
+ if (btn) {
1127
+ btn.disabled = false;
1128
+ btn.innerHTML = 'Update Password';
1129
+ }
1130
+
1131
+ modal.style.display = 'flex';
1132
+ }
1133
+
1134
+ function closePasswordModal() {
1135
+ const modal = document.getElementById('password-modal');
1136
+ if (modal) modal.style.display = 'none';
1137
+ }
1138
+
1139
+ async function submitNewPassword() {
1140
+ const current = document.getElementById('current-pass').value;
1141
+ const newPass = document.getElementById('new-pass').value;
1142
+ const errDiv = document.getElementById('pwd-error');
1143
+ const btn = document.getElementById('update-pass-btn');
1144
+
1145
+ if (!current || !newPass) {
1146
+ errDiv.textContent = 'All fields are required';
1147
+ errDiv.style.display = 'block';
1148
+ return;
1149
+ }
1150
+
1151
+ const originalHtml = btn.innerHTML;
1152
+ btn.disabled = true;
1153
+ btn.innerHTML = '<ion-icon name="sync-outline" class="spin"></ion-icon> Updating...';
1154
+ errDiv.style.display = 'none';
1155
+
1156
+ try {
1157
+ const res = await fetch('/api/change-password', {
1158
+ method: 'POST',
1159
+ headers: { 'Content-Type': 'application/json' },
1160
+ body: JSON.stringify({ currentPassword: current, newPassword: newPass })
1161
+ });
1162
+ const data = await res.json();
1163
+
1164
+ if (res.ok && data.status === 'success') {
1165
+ btn.innerHTML = '<ion-icon name="checkmark-circle-outline"></ion-icon> Success!';
1166
+ setTimeout(async () => {
1167
+ alert('Password updated. Please login again.');
1168
+ await fetch('/logout', { method: 'POST' });
1169
+ window.location.href = '/login';
1170
+ }, 1000);
1171
+ } else {
1172
+ throw new Error(data.message || 'Update failed');
1173
+ }
1174
+ } catch (e) {
1175
+ errDiv.textContent = e.message;
1176
+ errDiv.style.display = 'block';
1177
+ btn.innerHTML = originalHtml;
1178
+ btn.disabled = false;
1179
+ }
1180
+ }