krsyer-server-monitor-pro 1.0.28 → 1.0.29
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.
- package/monitor.js +7 -0
- package/package.json +7 -9
- package/DEPLOY_GUIDE.md +0 -68
- package/UPDATES.md +0 -84
- package/bin/cli.js +0 -84
- package/lib/controllers/cloudflareController.js +0 -1
- package/lib/controllers/dockerController.js +0 -1
- package/lib/controllers/networkController.js +0 -1
- package/lib/controllers/serverController.js +0 -1
- package/lib/ecosystem.config.js +0 -1
- package/lib/middleware/saasAuth.js +0 -1
- package/lib/public/login.html +0 -99
- package/lib/public/payment.html +0 -152
- package/lib/public/script.js +0 -1180
- package/lib/public/style.css +0 -1045
- package/lib/routes/cloudflareRoutes.js +0 -1
- package/lib/routes/dockerRoutes.js +0 -1
- package/lib/routes/networkRoutes.js +0 -1
- package/lib/routes/serverRoutes.js +0 -1
- package/lib/server.js +0 -1
- package/lib/services/cashfreeService.js +0 -1
- package/lib/views/activate.html +0 -109
- package/lib/views/index.html +0 -552
package/lib/public/script.js
DELETED
|
@@ -1,1180 +0,0 @@
|
|
|
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
|
-
}
|