j49_c commited on
Commit
a39ccf4
·
0 Parent(s):

first commit

Browse files
Files changed (8) hide show
  1. .gitignore +4 -0
  2. README.md +1 -0
  3. admin.html +483 -0
  4. app.py +981 -0
  5. create.html +252 -0
  6. index.html +1159 -0
  7. login.html +227 -0
  8. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ USERS/
3
+ *.pyc
4
+ .env
README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ # hostbot
admin.html ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>الذئب الابيض · @j49_c</title>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
10
+
11
+ :root {
12
+ --bg: #000000;
13
+ --bg-elevated: rgba(28, 28, 30, 0.6);
14
+ --border: rgba(255, 255, 255, 0.1);
15
+ --text: #F5F5F7;
16
+ --muted: #86868B;
17
+ --primary: #0A84FF;
18
+ --primary-hover: #0071E3;
19
+ --danger: #FF453A;
20
+ --ok: #32D74B;
21
+ --warn: #FF9F0A;
22
+ --shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
28
+ }
29
+
30
+ body {
31
+ margin: 0;
32
+ padding: 0;
33
+ color: var(--text);
34
+ background-color: var(--bg);
35
+ background-image:
36
+ radial-gradient(circle at 10% 10%, rgba(10, 132, 255, 0.1), transparent 50%),
37
+ radial-gradient(circle at 90% 90%, rgba(255, 69, 58, 0.05), transparent 50%);
38
+ background-attachment: fixed;
39
+ }
40
+
41
+ .top {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ padding: 16px 24px;
46
+ border-bottom: 1px solid var(--border);
47
+ background: rgba(0, 0, 0, 0.5);
48
+ backdrop-filter: blur(20px);
49
+ -webkit-backdrop-filter: blur(20px);
50
+ position: sticky;
51
+ top: 0;
52
+ z-index: 100;
53
+ }
54
+
55
+ .logo {
56
+ font-weight: 700;
57
+ font-size: 18px;
58
+ letter-spacing: -0.5px;
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ }
63
+
64
+ .logo span {
65
+ color: var(--muted);
66
+ font-weight: 400;
67
+ font-size: 13px;
68
+ letter-spacing: 0;
69
+ }
70
+
71
+ .btn {
72
+ padding: 10px 16px;
73
+ border-radius: 12px;
74
+ font-size: 14px;
75
+ font-weight: 600;
76
+ text-align: center;
77
+ cursor: pointer;
78
+ transition: all 0.2s;
79
+ border: none;
80
+ outline: none;
81
+ background: var(--bg-elevated);
82
+ color: var(--text);
83
+ border: 1px solid var(--border);
84
+ }
85
+
86
+ .btn:hover {
87
+ background: rgba(255, 255, 255, 0.08);
88
+ }
89
+
90
+ .btn-primary {
91
+ background: var(--primary);
92
+ color: white;
93
+ border: none;
94
+ }
95
+
96
+ .btn-primary:hover {
97
+ background: var(--primary-hover);
98
+ transform: translateY(-1px);
99
+ }
100
+
101
+ .btn-danger {
102
+ background: rgba(255, 69, 58, 0.15);
103
+ color: var(--danger);
104
+ border: 1px solid rgba(255, 69, 58, 0.3);
105
+ }
106
+
107
+ .btn-danger:hover {
108
+ background: rgba(255, 69, 58, 0.25);
109
+ }
110
+
111
+ .wrap {
112
+ max-width: 1200px;
113
+ margin: 32px auto;
114
+ padding: 0 24px;
115
+ animation: fadeUp 0.6s ease;
116
+ }
117
+
118
+ @keyframes fadeUp {
119
+ from {
120
+ opacity: 0;
121
+ transform: translateY(10px);
122
+ }
123
+
124
+ to {
125
+ opacity: 1;
126
+ transform: translateY(0);
127
+ }
128
+ }
129
+
130
+ .grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(4, 1fr);
133
+ gap: 16px;
134
+ }
135
+
136
+ @media (max-width: 900px) {
137
+ .grid {
138
+ grid-template-columns: repeat(2, 1fr);
139
+ }
140
+ }
141
+
142
+ @media (max-width: 520px) {
143
+ .grid {
144
+ grid-template-columns: 1fr;
145
+ }
146
+ }
147
+
148
+ .card {
149
+ background: var(--bg-elevated);
150
+ border: 1px solid var(--border);
151
+ border-radius: 20px;
152
+ padding: 20px;
153
+ box-shadow: var(--shadow);
154
+ backdrop-filter: blur(20px);
155
+ -webkit-backdrop-filter: blur(20px);
156
+ }
157
+
158
+ .k {
159
+ color: var(--muted);
160
+ font-size: 13px;
161
+ font-weight: 500;
162
+ }
163
+
164
+ .v {
165
+ font-size: 28px;
166
+ font-weight: 700;
167
+ margin-top: 8px;
168
+ letter-spacing: -0.5px;
169
+ }
170
+
171
+ .card-info {
172
+ font-size: 12px;
173
+ color: var(--muted);
174
+ line-height: 1.5;
175
+ margin-top: 6px;
176
+ }
177
+
178
+ h2 {
179
+ margin: 40px 0 16px;
180
+ font-size: 20px;
181
+ font-weight: 600;
182
+ letter-spacing: -0.5px;
183
+ }
184
+
185
+ .table-wrapper {
186
+ background: var(--bg-elevated);
187
+ border: 1px solid var(--border);
188
+ border-radius: 20px;
189
+ overflow: hidden;
190
+ box-shadow: var(--shadow);
191
+ backdrop-filter: blur(20px);
192
+ -webkit-backdrop-filter: blur(20px);
193
+ overflow-x: auto;
194
+ }
195
+
196
+ table {
197
+ width: 100%;
198
+ border-collapse: collapse;
199
+ text-align: left;
200
+ }
201
+
202
+ th,
203
+ td {
204
+ padding: 14px 20px;
205
+ border-bottom: 1px solid var(--border);
206
+ font-size: 14px;
207
+ }
208
+
209
+ th {
210
+ color: var(--muted);
211
+ font-size: 12px;
212
+ font-weight: 500;
213
+ text-transform: uppercase;
214
+ letter-spacing: 0.5px;
215
+ background: rgba(0, 0, 0, 0.2);
216
+ }
217
+
218
+ tr:last-child td {
219
+ border-bottom: none;
220
+ }
221
+
222
+ .tag {
223
+ display: inline-block;
224
+ padding: 4px 10px;
225
+ border-radius: 8px;
226
+ font-size: 11px;
227
+ font-weight: 600;
228
+ background: rgba(0, 0, 0, 0.4);
229
+ border: 1px solid var(--border);
230
+ text-transform: uppercase;
231
+ }
232
+
233
+ .tag.ok {
234
+ color: var(--ok);
235
+ border-color: rgba(50, 215, 75, 0.3);
236
+ background: rgba(50, 215, 75, 0.1);
237
+ }
238
+
239
+ .tag.bad {
240
+ color: var(--danger);
241
+ border-color: rgba(255, 69, 58, 0.3);
242
+ background: rgba(255, 69, 58, 0.1);
243
+ }
244
+
245
+ .tag.mid {
246
+ color: var(--warn);
247
+ border-color: rgba(255, 159, 10, 0.3);
248
+ background: rgba(255, 159, 10, 0.1);
249
+ }
250
+
251
+ .actions {
252
+ display: flex;
253
+ gap: 8px;
254
+ flex-wrap: wrap;
255
+ }
256
+
257
+ .row {
258
+ display: flex;
259
+ gap: 12px;
260
+ flex-wrap: wrap;
261
+ align-items: center;
262
+ margin-bottom: 16px;
263
+ }
264
+
265
+ input {
266
+ padding: 12px 16px;
267
+ border-radius: 12px;
268
+ border: 1px solid var(--border);
269
+ background: rgba(0, 0, 0, 0.4);
270
+ color: var(--text);
271
+ outline: none;
272
+ font-size: 14px;
273
+ width: 100%;
274
+ max-width: 300px;
275
+ transition: all 0.2s;
276
+ }
277
+
278
+ input:focus {
279
+ border-color: var(--primary);
280
+ }
281
+
282
+ .msg-bar {
283
+ display: inline-block;
284
+ margin-left: 12px;
285
+ color: var(--muted);
286
+ font-size: 13px;
287
+ }
288
+ </style>
289
+ </head>
290
+
291
+ <body>
292
+ <div class="top">
293
+ <div class="logo">الذئب الابيض <span>CENTRAL COMMAND</span></div>
294
+ <div style="display:flex;gap:12px;">
295
+ <button class="btn" onclick="location.href='/'">Operative Dashboard</button>
296
+ <button class="btn btn-danger" onclick="location.href='/logout'">Terminate Session</button>
297
+ </div>
298
+ </div>
299
+
300
+ <div class="wrap">
301
+ <div class="grid">
302
+ <div class="card">
303
+ <div class="k">Containers Total</div>
304
+ <div class="v" id="st_total">0</div>
305
+ </div>
306
+ <div class="card">
307
+ <div class="k">Online Nodes</div>
308
+ <div class="v" id="st_run">0</div>
309
+ <div class="card-info" style="color:var(--ok)">Actively connected</div>
310
+ </div>
311
+ <div class="card">
312
+ <div class="k">Booting / Pending</div>
313
+ <div class="v" id="st_ins">0</div>
314
+ <div class="card-info" style="color:var(--warn)">Awaiting handshake</div>
315
+ </div>
316
+ <div class="card">
317
+ <div class="k">Quarantined</div>
318
+ <div class="v" id="st_ban">0</div>
319
+ <div class="card-info" style="color:var(--danger)">Restricted access</div>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="grid" style="margin-top:20px">
324
+ <div class="card">
325
+ <div class="k">Total Operatives</div>
326
+ <div class="v" id="u_total">0</div>
327
+ </div>
328
+ <div class="card">
329
+ <div class="k">Active Clearances</div>
330
+ <div class="v" id="u_active">0</div>
331
+ </div>
332
+ <div class="card">
333
+ <div class="k">Level 5 Clearance</div>
334
+ <div class="v" id="u_premium">0</div>
335
+ </div>
336
+ <div class="card" style="display:flex; flex-direction:column; justify-content:center;">
337
+ <div class="k">Protocols</div>
338
+ <div class="card-info" style="font-size:13px;">Standard: 1 Container<br>Level 5: 5 Containers</div>
339
+ </div>
340
+ </div>
341
+
342
+ <div class="row" style="margin-top:32px;">
343
+ <button class="btn btn-primary" onclick="refreshAll()">Sync Data</button>
344
+ <span class="msg-bar" id="msg">Authenticating...</span>
345
+ </div>
346
+
347
+ <h2>Node Directory</h2>
348
+ <div class="table-wrapper">
349
+ <table>
350
+ <thead>
351
+ <tr>
352
+ <th>Identifier</th>
353
+ <th>Alias</th>
354
+ <th>Operative</th>
355
+ <th>Main Routine</th>
356
+ <th>Status</th>
357
+ <th>Override</th>
358
+ </tr>
359
+ </thead>
360
+ <tbody id="serversT"></tbody>
361
+ </table>
362
+ </div>
363
+
364
+ <h2 style="margin-top:40px">Operative Roster</h2>
365
+ <div class="row">
366
+ <input id="searchUser" placeholder="Search by Designation..." oninput="filterUsers()">
367
+ </div>
368
+ <div class="table-wrapper">
369
+ <table>
370
+ <thead>
371
+ <tr>
372
+ <th>Designation</th>
373
+ <th>Comm Line (Email)</th>
374
+ <th>Nodes</th>
375
+ <th>Access</th>
376
+ <th>Clearance</th>
377
+ <th>Directives</th>
378
+ </tr>
379
+ </thead>
380
+ <tbody id="usersT"></tbody>
381
+ </table>
382
+ </div>
383
+ </div>
384
+
385
+ <script>
386
+ let usersCache = [];
387
+
388
+ function tagStatus(s) {
389
+ const st = (s || '').toLowerCase();
390
+ if (st === 'running') return '<span class="tag ok">ONLINE</span>';
391
+ if (st === 'installing' || st === 'starting') return '<span class="tag mid">BOOTING</span>';
392
+ if (st === 'banned') return '<span class="tag bad">QUARANTINE</span>';
393
+ return '<span class="tag">OFFLINE</span>';
394
+ }
395
+
396
+ async function refreshStats() {
397
+ const r = await fetch('/api/admin/quickstats');
398
+ const d = await r.json();
399
+ const s = d.stats || {};
400
+ document.getElementById('st_total').textContent = s.servers_total || 0;
401
+ document.getElementById('st_run').textContent = s.servers_running || 0;
402
+ document.getElementById('st_ins').textContent = s.servers_installing || 0;
403
+ document.getElementById('st_ban').textContent = s.servers_banned || 0;
404
+ document.getElementById('u_total').textContent = s.users_total || 0;
405
+ document.getElementById('u_active').textContent = s.users_active || 0;
406
+ document.getElementById('u_premium').textContent = s.users_premium || 0;
407
+ }
408
+
409
+ async function refreshServers() {
410
+ const r = await fetch('/api/admin/servers');
411
+ const d = await r.json();
412
+ const t = document.getElementById('serversT'); t.innerHTML = '';
413
+ (d.servers || []).forEach(s => {
414
+ t.innerHTML += `
415
+ <tr>
416
+ <td style="font-family:monospace; color:var(--muted)">${s.folder}</td>
417
+ <td style="font-weight:600">${s.display_name}</td>
418
+ <td>${s.owner}</td>
419
+ <td style="font-family:monospace">${s.startup_file || '—'}</td>
420
+ <td>${tagStatus(s.status)}</td>
421
+ <td class="actions">
422
+ ${s.banned
423
+ ? `<button class="btn" onclick="banServer('${s.key}', false)">Authorize</button>`
424
+ : `<button class="btn btn-danger" onclick="banServer('${s.key}', true)">Quarantine (Ban)</button>`}
425
+ </td>
426
+ </tr>`;
427
+ });
428
+ }
429
+
430
+ async function banServer(key, banned) {
431
+ await fetch('/api/admin/server/ban', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, banned }) });
432
+ refreshAll();
433
+ }
434
+
435
+ async function refreshUsers() {
436
+ const r = await fetch('/api/admin/users');
437
+ const d = await r.json();
438
+ usersCache = d.users || [];
439
+ renderUsers(usersCache);
440
+ }
441
+
442
+ function renderUsers(arr) {
443
+ const t = document.getElementById('usersT'); t.innerHTML = '';
444
+ arr.forEach(u => {
445
+ t.innerHTML += `
446
+ <tr>
447
+ <td style="font-weight:600">${u.username}</td>
448
+ <td style="color:var(--muted)">${u.email || '—'}</td>
449
+ <td>${u.servers}</td>
450
+ <td>${u.active ? '<span class="tag ok">ACTIVE</span>' : '<span class="tag bad">REVOKED</span>'}</td>
451
+ <td>${u.premium ? '<span class="tag primary" style="background:rgba(10,132,255,0.2);color:var(--primary);border-color:rgba(10,132,255,0.4)">LEVEL 5</span>' : '<span class="tag">STANDARD</span>'}</td>
452
+ <td class="actions">
453
+ <button class="btn ${u.active ? 'btn-danger' : ''}" onclick="setActive('${u.username}', ${u.active ? 'false' : 'true'})">${u.active ? 'Revoke' : 'Reinstate'}</button>
454
+ <button class="btn" style="border-style:dashed" onclick="setPremium('${u.username}', ${u.premium ? 'false' : 'true'})">${u.premium ? '- Demote' : '+ Grant LVL5'}</button>
455
+ </td>
456
+ </tr>`;
457
+ });
458
+ }
459
+
460
+ async function setActive(username, active) {
461
+ await fetch('/api/admin/user/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, active }) });
462
+ refreshUsers(); refreshStats();
463
+ }
464
+ async function setPremium(username, premium) {
465
+ await fetch('/api/admin/user/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, premium }) });
466
+ refreshUsers(); refreshStats();
467
+ }
468
+ function filterUsers() {
469
+ const q = (document.getElementById('searchUser').value || '').toLowerCase().trim();
470
+ if (!q) return renderUsers(usersCache);
471
+ renderUsers(usersCache.filter(u => (u.username || '').toLowerCase().includes(q)));
472
+ }
473
+
474
+ async function refreshAll() {
475
+ document.getElementById('msg').textContent = 'Syncing to mainframe...';
476
+ await Promise.all([refreshStats(), refreshServers(), refreshUsers()]);
477
+ document.getElementById('msg').innerHTML = '<span style="color:var(--ok)">Sync Complete.</span>';
478
+ }
479
+ document.addEventListener('DOMContentLoaded', refreshAll);
480
+ </script>
481
+ </body>
482
+
483
+ </html>
app.py ADDED
@@ -0,0 +1,981 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import shutil
5
+ import socket
6
+ import hashlib
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ import sys
11
+
12
+ import psutil
13
+ from flask import Flask, send_from_directory, request, jsonify, redirect, session
14
+ from werkzeug.security import generate_password_hash, check_password_hash
15
+ from functools import wraps
16
+
17
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
18
+
19
+ USERS_ROOT = os.path.join(BASE_DIR, "USERS")
20
+ DATA_DIR = os.path.join(BASE_DIR, "DATA")
21
+ USERS_DB = os.path.join(DATA_DIR, "users.json")
22
+
23
+ os.makedirs(USERS_ROOT, exist_ok=True)
24
+ os.makedirs(DATA_DIR, exist_ok=True)
25
+
26
+ app = Flask(__name__)
27
+ app.secret_key = os.environ.get("PANEL_SECRET_KEY", "CHANGE_ME_" + os.urandom(16).hex())
28
+
29
+ ADMIN_USERNAME = os.environ.get("ADMIN_USER", "moh777")
30
+ ADMIN_PASSWORD = os.environ.get("ADMIN_PASS", "Mm@123456")
31
+
32
+ running_procs = {}
33
+ server_states = {}
34
+ lock = threading.Lock()
35
+
36
+
37
+ def get_ip():
38
+ try:
39
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
40
+ s.connect(("10.255.255.255", 1))
41
+ ip = s.getsockname()[0]
42
+ s.close()
43
+ return ip
44
+ except Exception:
45
+ return "127.0.0.1"
46
+
47
+
48
+ def sanitize_folder_name(name: str) -> str:
49
+ name = (name or "").strip()
50
+ name = re.sub(r"\s+", "-", name)
51
+ name = re.sub(r"[^A-Za-z0-9\-_\.]", "", name)
52
+ return name[:200]
53
+
54
+
55
+ def safe_name(name: str) -> str:
56
+ name = (name or "").strip()
57
+ name = re.sub(r"[\\/]+", "", name)
58
+ name = re.sub(r"[^A-Za-z0-9\-_\. ]", "", name)
59
+ return name[:200].strip()
60
+
61
+
62
+ def set_state(key: str, state: str):
63
+ with lock:
64
+ server_states[key] = state
65
+
66
+
67
+ def get_state(key: str) -> str:
68
+ with lock:
69
+ return server_states.get(key, "Offline")
70
+
71
+
72
+ def log_append(key: str, text: str):
73
+ try:
74
+ owner, folder = parse_server_key(key, allow_admin=True)
75
+ p = os.path.join(get_server_dir(owner, folder), "server.log")
76
+ with open(p, "a", encoding="utf-8", errors="ignore") as f:
77
+ f.write(text)
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ def load_users():
83
+ if not os.path.exists(USERS_DB):
84
+ return {"users": []}
85
+ try:
86
+ with open(USERS_DB, "r", encoding="utf-8") as f:
87
+ return json.load(f) or {"users": []}
88
+ except Exception:
89
+ return {"users": []}
90
+
91
+
92
+ def save_users(db):
93
+ tmp = USERS_DB + ".tmp"
94
+ with open(tmp, "w", encoding="utf-8") as f:
95
+ json.dump(db, f, indent=2)
96
+ os.replace(tmp, USERS_DB)
97
+
98
+
99
+ def find_user(db, username: str):
100
+ u = (username or "").strip().lower()
101
+ for x in db.get("users", []):
102
+ if (x.get("username") or "").strip().lower() == u:
103
+ return x
104
+ return None
105
+
106
+
107
+ def is_admin_session():
108
+ u = session.get("user") or {}
109
+ return bool(u.get("is_admin"))
110
+
111
+
112
+ def current_username():
113
+ u = session.get("user") or {}
114
+ return (u.get("username") or "").strip()
115
+
116
+
117
+ def get_user_limit(username: str) -> int:
118
+ if is_admin_session():
119
+ return 999999
120
+ db = load_users()
121
+ u = find_user(db, username)
122
+ if not u:
123
+ return 1
124
+ return 5 if u.get("premium", False) else 1
125
+
126
+
127
+ def login_required(fn):
128
+ @wraps(fn)
129
+ def wrapper(*args, **kwargs):
130
+ if not session.get("user"):
131
+ return redirect("/login")
132
+ return fn(*args, **kwargs)
133
+ return wrapper
134
+
135
+
136
+ def admin_required(fn):
137
+ @wraps(fn)
138
+ def wrapper(*args, **kwargs):
139
+ if not session.get("user"):
140
+ return redirect("/login")
141
+ if not is_admin_session():
142
+ return jsonify({"success": False, "message": "Admin only"}), 403
143
+ return fn(*args, **kwargs)
144
+ return wrapper
145
+
146
+
147
+ def get_user_servers_root(username: str) -> str:
148
+ return os.path.join(USERS_ROOT, username, "servers")
149
+
150
+
151
+ def get_server_dir(owner: str, folder: str) -> str:
152
+ return os.path.join(get_user_servers_root(owner), folder)
153
+
154
+
155
+ def ensure_user_dirs(username: str):
156
+ os.makedirs(get_user_servers_root(username), exist_ok=True)
157
+
158
+
159
+ def parse_server_key(key: str, allow_admin: bool):
160
+ key = (key or "").strip()
161
+
162
+ if "::" in key:
163
+ owner, folder = key.split("::", 1)
164
+ owner = owner.strip()
165
+ folder = folder.strip()
166
+
167
+ if not allow_admin:
168
+ raise ValueError("not allowed")
169
+
170
+ if not is_admin_session():
171
+ raise ValueError("forbidden")
172
+ return owner, folder
173
+
174
+ return current_username(), key
175
+
176
+
177
+ def can_access_key(key: str) -> bool:
178
+ try:
179
+ owner, folder = parse_server_key(key, allow_admin=True)
180
+ except Exception:
181
+ return False
182
+ if is_admin_session():
183
+ return True
184
+ return owner == current_username()
185
+
186
+
187
+ def safe_join_server_path(key: str, rel_path: str = "") -> str:
188
+ owner, folder = parse_server_key(key, allow_admin=True)
189
+
190
+ root = os.path.abspath(get_server_dir(owner, folder))
191
+ rel_path = (rel_path or "").replace("\\", "/").strip()
192
+ if rel_path.startswith("/") or rel_path.startswith("~"):
193
+ rel_path = rel_path.lstrip("/").lstrip("~")
194
+
195
+ joined = os.path.abspath(os.path.join(root, rel_path))
196
+ if not (joined == root or joined.startswith(root + os.sep)):
197
+ raise ValueError("Invalid path")
198
+ return joined
199
+
200
+
201
+ def ensure_meta(owner: str, folder: str):
202
+ server_dir = get_server_dir(owner, folder)
203
+ os.makedirs(server_dir, exist_ok=True)
204
+ meta_path = os.path.join(server_dir, "meta.json")
205
+ base = {"display_name": folder, "startup_file": "", "owner": owner, "banned": False}
206
+ if not os.path.exists(meta_path):
207
+ with open(meta_path, "w", encoding="utf-8") as f:
208
+ json.dump(base, f, indent=2)
209
+ else:
210
+ try:
211
+ with open(meta_path, "r", encoding="utf-8") as f:
212
+ m = json.load(f) or {}
213
+ except Exception:
214
+ m = {}
215
+ changed = False
216
+ for k, v in base.items():
217
+ if k not in m:
218
+ m[k] = v
219
+ changed = True
220
+ if m.get("owner") != owner:
221
+ m["owner"] = owner
222
+ changed = True
223
+ if changed:
224
+ with open(meta_path, "w", encoding="utf-8") as f:
225
+ json.dump(m, f, indent=2)
226
+ return meta_path
227
+
228
+
229
+ def read_meta(owner: str, folder: str):
230
+ meta_path = ensure_meta(owner, folder)
231
+ try:
232
+ with open(meta_path, "r", encoding="utf-8") as f:
233
+ return json.load(f) or {}
234
+ except Exception:
235
+ return {"display_name": folder, "startup_file": "", "owner": owner, "banned": False}
236
+
237
+
238
+ def write_meta(owner: str, folder: str, meta):
239
+ meta_path = ensure_meta(owner, folder)
240
+ with open(meta_path, "w", encoding="utf-8") as f:
241
+ json.dump(meta, f, indent=2)
242
+
243
+
244
+ def sha256_file(path):
245
+ h = hashlib.sha256()
246
+ with open(path, "rb") as f:
247
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
248
+ h.update(chunk)
249
+ return h.hexdigest()
250
+
251
+
252
+ def installed_file_path(owner: str, folder: str):
253
+ return os.path.join(get_server_dir(owner, folder), ".installed")
254
+
255
+
256
+ def read_installed(owner: str, folder: str):
257
+ p = installed_file_path(owner, folder)
258
+ data = {"req_sha": "", "pkgs": set()}
259
+ if not os.path.exists(p):
260
+ return data
261
+ try:
262
+ with open(p, "r", encoding="utf-8", errors="ignore") as f:
263
+ for line in f.read().splitlines():
264
+ line = line.strip()
265
+ if not line:
266
+ continue
267
+ if line.startswith("REQ_SHA="):
268
+ data["req_sha"] = line.split("=", 1)[1].strip()
269
+ else:
270
+ data["pkgs"].add(line)
271
+ except Exception:
272
+ pass
273
+ return data
274
+
275
+
276
+ def write_installed(owner: str, folder: str, req_sha=None, add_pkgs=None):
277
+ p = installed_file_path(owner, folder)
278
+ cur = read_installed(owner, folder)
279
+ if req_sha is not None:
280
+ cur["req_sha"] = req_sha
281
+ if add_pkgs:
282
+ cur["pkgs"].update(add_pkgs)
283
+ lines = []
284
+ if cur["req_sha"]:
285
+ lines.append(f"REQ_SHA={cur['req_sha']}")
286
+ lines.extend(sorted(cur["pkgs"]))
287
+ with open(p, "w", encoding="utf-8") as f:
288
+ f.write("\n".join(lines) + ("\n" if lines else ""))
289
+
290
+
291
+ def ensure_requirements_installed(owner: str, folder: str):
292
+ server_dir = get_server_dir(owner, folder)
293
+ req_path = os.path.join(server_dir, "requirements.txt")
294
+ if not os.path.exists(req_path):
295
+ return False
296
+
297
+ req_sha = sha256_file(req_path)
298
+ cur = read_installed(owner, folder)
299
+ if cur["req_sha"] == req_sha:
300
+ return False
301
+
302
+ log_append(f"{owner}::{folder}", "[SYSTEM] Installing requirements.txt...\n")
303
+ try:
304
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=server_dir)
305
+ write_installed(owner, folder, req_sha=req_sha)
306
+ log_append(f"{owner}::{folder}", "[SYSTEM] requirements installed ✅\n")
307
+ return True
308
+ except subprocess.CalledProcessError as e:
309
+ log_append(f"{owner}::{folder}", f"[SYSTEM] requirements install failed: {e}\n")
310
+ return False
311
+
312
+
313
+ def start_with_autoinstall(owner: str, folder: str, startup_file: str):
314
+ wrapper_code = r'''
315
+ import runpy, sys, subprocess, traceback, re, os
316
+ script = sys.argv[1]
317
+ cwd = os.getcwd()
318
+
319
+ def append_installed(pkg):
320
+ try:
321
+ p = os.path.join(cwd, ".installed")
322
+ existing = set()
323
+ if os.path.exists(p):
324
+ with open(p, "r", encoding="utf-8", errors="ignore") as f:
325
+ existing = set([x.strip() for x in f.read().splitlines() if x.strip()])
326
+ if pkg and pkg not in existing:
327
+ with open(p, "a", encoding="utf-8") as f:
328
+ f.write(pkg + "\n")
329
+ except:
330
+ pass
331
+
332
+ def parse_missing_name(e):
333
+ n = getattr(e, "name", None)
334
+ if n: return n
335
+ s = str(e)
336
+ m = re.search(r"No module named '([^']+)'", s)
337
+ if m: return m.group(1)
338
+ return None
339
+
340
+ while True:
341
+ try:
342
+ runpy.run_path(script, run_name="__main__")
343
+ break
344
+ except ModuleNotFoundError as e:
345
+ pkg = parse_missing_name(e)
346
+ if not pkg:
347
+ traceback.print_exc()
348
+ break
349
+ print(f"[AUTO-INSTALL] Missing module: {pkg} -> installing...")
350
+ try:
351
+ subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
352
+ append_installed(pkg)
353
+ print(f"[AUTO-INSTALL] Installed: {pkg} ✅ -> restarting...")
354
+ continue
355
+ except Exception as ex:
356
+ print(f"[AUTO-INSTALL] Failed: {ex}")
357
+ traceback.print_exc()
358
+ break
359
+ except Exception:
360
+ traceback.print_exc()
361
+ break
362
+ '''
363
+ server_dir = get_server_dir(owner, folder)
364
+ log_path = os.path.join(server_dir, "server.log")
365
+ log_file = open(log_path, "a", encoding="utf-8", errors="ignore")
366
+
367
+ proc = subprocess.Popen(
368
+ [sys.executable, "-u", "-c", wrapper_code, startup_file],
369
+ cwd=server_dir,
370
+ stdout=log_file,
371
+ stderr=log_file,
372
+ )
373
+ return proc, log_file
374
+
375
+
376
+ def stop_proc(key: str):
377
+ if key in running_procs:
378
+ proc, logf = running_procs[key]
379
+ try:
380
+ p = psutil.Process(proc.pid)
381
+ for child in p.children(recursive=True):
382
+ child.kill()
383
+ p.kill()
384
+ except Exception:
385
+ pass
386
+ try:
387
+ logf.close()
388
+ except Exception:
389
+ pass
390
+ running_procs.pop(key, None)
391
+
392
+
393
+ @app.route("/")
394
+ @login_required
395
+ def home():
396
+ return send_from_directory(BASE_DIR, "index.html")
397
+
398
+
399
+ @app.route("/login")
400
+ def login_page():
401
+ return send_from_directory(BASE_DIR, "login.html")
402
+
403
+
404
+ @app.route("/create")
405
+ def create_page():
406
+ return send_from_directory(BASE_DIR, "create.html")
407
+
408
+
409
+ @app.route("/admin")
410
+ @login_required
411
+ def admin_page():
412
+ if not is_admin_session():
413
+ return redirect("/")
414
+ return send_from_directory(BASE_DIR, "admin.html")
415
+
416
+
417
+ @app.route("/logout")
418
+ def logout():
419
+ session.pop("user", None)
420
+ return redirect("/login")
421
+
422
+
423
+ @app.route("/api/auth/login", methods=["POST"])
424
+ def api_login():
425
+ data = request.get_json(silent=True) or {}
426
+ username = (data.get("username") or "").strip()
427
+ password = data.get("password") or ""
428
+
429
+ if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
430
+ session["user"] = {"username": ADMIN_USERNAME, "is_admin": True}
431
+ return jsonify({"success": True, "is_admin": True})
432
+
433
+ db = load_users()
434
+ u = find_user(db, username)
435
+ if not u:
436
+ return jsonify({"success": False, "message": "Invalid username or password"}), 401
437
+ if not u.get("active", True):
438
+ return jsonify({"success": False, "message": "Account is banned / inactive"}), 403
439
+ if not check_password_hash(u.get("password_hash", ""), password):
440
+ return jsonify({"success": False, "message": "Invalid username or password"}), 401
441
+
442
+ session["user"] = {"username": u.get("username"), "is_admin": False}
443
+ ensure_user_dirs(u.get("username"))
444
+ return jsonify({"success": True, "is_admin": False})
445
+
446
+
447
+ @app.route("/api/auth/create", methods=["POST"])
448
+ def api_create():
449
+ return jsonify({"success": False, "message": "Registration disabled. Personal use only."}), 403
450
+
451
+
452
+ def list_all_servers_for_admin():
453
+ servers = []
454
+ if not os.path.isdir(USERS_ROOT):
455
+ return servers
456
+
457
+ for owner in sorted(os.listdir(USERS_ROOT)):
458
+ root = get_user_servers_root(owner)
459
+ if not os.path.isdir(root):
460
+ continue
461
+ for folder in sorted(os.listdir(root)):
462
+ server_dir = get_server_dir(owner, folder)
463
+ if not os.path.isdir(server_dir):
464
+ continue
465
+ meta = read_meta(owner, folder)
466
+ banned = bool(meta.get("banned", False))
467
+ key = f"{owner}::{folder}"
468
+ st = "Banned" if banned else get_state(key)
469
+ servers.append({
470
+ "title": meta.get("display_name", folder),
471
+ "folder": folder,
472
+ "owner": owner,
473
+ "key": key,
474
+ "subtitle": f"Owner: {owner}",
475
+ "startup_file": meta.get("startup_file", ""),
476
+ "status": st
477
+ })
478
+ return servers
479
+
480
+
481
+ def list_servers_for_user(username: str):
482
+ ensure_user_dirs(username)
483
+ root = get_user_servers_root(username)
484
+ servers = []
485
+ for folder in sorted(os.listdir(root)):
486
+ server_dir = get_server_dir(username, folder)
487
+ if not os.path.isdir(server_dir):
488
+ continue
489
+ meta = read_meta(username, folder)
490
+ banned = bool(meta.get("banned", False))
491
+ key = folder
492
+ st = "Banned" if banned else get_state(key)
493
+ servers.append({
494
+ "title": meta.get("display_name", folder),
495
+ "folder": folder,
496
+ "owner": username,
497
+ "key": key,
498
+ "subtitle": f"Owner: {username}",
499
+ "startup_file": meta.get("startup_file", ""),
500
+ "status": st
501
+ })
502
+ return servers
503
+
504
+
505
+ @app.route("/servers")
506
+ @login_required
507
+ def servers():
508
+ if is_admin_session():
509
+ return jsonify({"success": True, "servers": list_all_servers_for_admin()})
510
+ return jsonify({"success": True, "servers": list_servers_for_user(current_username())})
511
+
512
+
513
+ @app.route("/add", methods=["POST"])
514
+ @login_required
515
+ def add_server():
516
+ data = request.get_json(silent=True) or {}
517
+ name = (data.get("name") or "").strip()
518
+ folder = sanitize_folder_name(name)
519
+ if not folder:
520
+ return jsonify({"success": False, "message": "Invalid server name"}), 400
521
+
522
+ if is_admin_session():
523
+ owner = current_username()
524
+ else:
525
+ owner = current_username()
526
+
527
+ ensure_user_dirs(owner)
528
+
529
+ if not is_admin_session():
530
+ limit = get_user_limit(owner)
531
+ existing = [d for d in os.listdir(get_user_servers_root(owner)) if os.path.isdir(get_server_dir(owner, d))]
532
+ if len(existing) >= limit:
533
+ return jsonify({"success": False, "message": f"Server limit reached ({limit}). Ask admin for premium."}), 403
534
+
535
+ target = get_server_dir(owner, folder)
536
+ if os.path.exists(target):
537
+ return jsonify({"success": False, "message": "Server already exists"}), 409
538
+
539
+ os.makedirs(target, exist_ok=True)
540
+ open(os.path.join(target, "server.log"), "w", encoding="utf-8").close()
541
+
542
+ meta = {
543
+ "display_name": name or folder,
544
+ "startup_file": "",
545
+ "owner": owner,
546
+ "banned": False
547
+ }
548
+ write_meta(owner, folder, meta)
549
+
550
+ set_state(folder if not is_admin_session() else f"{owner}::{folder}", "Offline")
551
+
552
+ if is_admin_session():
553
+ return jsonify({"success": True, "servers": list_all_servers_for_admin()})
554
+ return jsonify({"success": True, "servers": list_servers_for_user(owner)})
555
+
556
+
557
+ @app.route("/server/stats/<path:key>")
558
+ @login_required
559
+ def server_stats(key):
560
+ if not can_access_key(key):
561
+ return jsonify({"success": False, "message": "Forbidden"}), 403
562
+
563
+ owner, folder = parse_server_key(key, allow_admin=True)
564
+ server_dir = get_server_dir(owner, folder)
565
+ if not os.path.isdir(server_dir):
566
+ return jsonify({"status": "Offline", "cpu": "0%", "mem": "0 MB", "logs": "", "ip": get_ip()}), 404
567
+
568
+ meta = read_meta(owner, folder)
569
+ if meta.get("banned", False):
570
+ set_state(key, "Banned")
571
+
572
+ proc_tuple = running_procs.get(key)
573
+ running = False
574
+ cpu, mem = "0%", "0 MB"
575
+
576
+ if proc_tuple:
577
+ proc, _logf = proc_tuple
578
+ if psutil.pid_exists(proc.pid):
579
+ try:
580
+ p = psutil.Process(proc.pid)
581
+ if p.is_running() and p.status() != psutil.STATUS_ZOMBIE:
582
+ running = True
583
+ cpu = f"{p.cpu_percent(interval=None)}%"
584
+ mem = f"{p.memory_info().rss / 1024 / 1024:.1f} MB"
585
+ except Exception:
586
+ pass
587
+
588
+ log_path = os.path.join(server_dir, "server.log")
589
+ try:
590
+ logs = open(log_path, "r", encoding="utf-8", errors="ignore").read() if os.path.exists(log_path) else ""
591
+ except Exception:
592
+ logs = ""
593
+
594
+ state = get_state(key)
595
+ if meta.get("banned", False):
596
+ state = "Banned"
597
+ elif running:
598
+ state = "Running"
599
+ set_state(key, "Running")
600
+ elif state not in ("Installing", "Starting"):
601
+ state = "Offline"
602
+ set_state(key, "Offline")
603
+
604
+ return jsonify({"status": state, "cpu": cpu, "mem": mem, "logs": logs, "ip": get_ip()})
605
+
606
+
607
+ def background_start(key: str, owner: str, folder: str, startup_file: str):
608
+ try:
609
+ set_state(key, "Installing")
610
+ log_append(key, "[SYSTEM] Preparing...\n")
611
+
612
+ ensure_requirements_installed(owner, folder)
613
+
614
+ set_state(key, "Starting")
615
+ log_append(key, "[SYSTEM] Starting...\n")
616
+
617
+ proc, logf = start_with_autoinstall(owner, folder, startup_file)
618
+ running_procs[key] = (proc, logf)
619
+
620
+ time.sleep(1.0)
621
+ if proc.poll() is None:
622
+ set_state(key, "Running")
623
+ else:
624
+ set_state(key, "Offline")
625
+ except Exception as e:
626
+ log_append(key, f"[SYSTEM] Start failed: {e}\n")
627
+ set_state(key, "Offline")
628
+
629
+
630
+ @app.route("/server/action/<path:key>/<act>", methods=["POST"])
631
+ @login_required
632
+ def server_action(key, act):
633
+ if not can_access_key(key):
634
+ return jsonify({"success": False, "message": "Forbidden"}), 403
635
+
636
+ owner, folder = parse_server_key(key, allow_admin=True)
637
+ server_dir = get_server_dir(owner, folder)
638
+ if not os.path.isdir(server_dir):
639
+ return jsonify({"success": False, "message": "Server not found"}), 404
640
+
641
+ meta = read_meta(owner, folder)
642
+ if meta.get("banned", False):
643
+ set_state(key, "Banned")
644
+ return jsonify({"success": False, "message": "Server is banned by admin"}), 403
645
+
646
+ if act in ("stop", "restart"):
647
+ stop_proc(key)
648
+ set_state(key, "Offline")
649
+
650
+ if act == "stop":
651
+ return jsonify({"success": True})
652
+
653
+ startup = meta.get("startup_file") or ""
654
+ if not startup:
655
+ return jsonify({"success": False, "message": "No main file set"}), 400
656
+
657
+ open(os.path.join(server_dir, "server.log"), "w", encoding="utf-8").close()
658
+
659
+ t = threading.Thread(target=background_start, args=(key, owner, folder, startup), daemon=True)
660
+ t.start()
661
+ return jsonify({"success": True})
662
+
663
+
664
+ @app.route("/server/set-startup/<path:key>", methods=["POST"])
665
+ @login_required
666
+ def set_startup(key):
667
+ if not can_access_key(key):
668
+ return jsonify({"success": False, "message": "Forbidden"}), 403
669
+
670
+ owner, folder = parse_server_key(key, allow_admin=True)
671
+ server_dir = get_server_dir(owner, folder)
672
+ if not os.path.isdir(server_dir):
673
+ return jsonify({"success": False, "message": "Server not found"}), 404
674
+
675
+ data = request.get_json(silent=True) or {}
676
+ f = (data.get("file") or "").strip()
677
+ meta = read_meta(owner, folder)
678
+ meta["startup_file"] = f
679
+ write_meta(owner, folder, meta)
680
+ return jsonify({"success": True})
681
+
682
+
683
+ @app.route("/files/list/<path:key>")
684
+ @login_required
685
+ def files_list(key):
686
+ if not can_access_key(key):
687
+ return jsonify({"success": False, "message": "Forbidden", "path": ""}), 403
688
+
689
+ rel = request.args.get("path", "") or ""
690
+ try:
691
+ base = safe_join_server_path(key, rel)
692
+ except Exception:
693
+ return jsonify({"success": False, "message": "Invalid path", "path": ""}), 400
694
+
695
+ dirs, files = [], []
696
+ if os.path.isdir(base):
697
+ for name in sorted(os.listdir(base), key=lambda x: (not os.path.isdir(os.path.join(base, x)), x.lower())):
698
+ if rel == "" and name in ("meta.json", "server.log"):
699
+ continue
700
+ full = os.path.join(base, name)
701
+ if os.path.isdir(full):
702
+ dirs.append({"name": name})
703
+ elif os.path.isfile(full):
704
+ try:
705
+ size_kb = os.path.getsize(full) / 1024
706
+ size = f"{size_kb:.1f} KB"
707
+ except Exception:
708
+ size = ""
709
+ files.append({"name": name, "size": size})
710
+
711
+ return jsonify({"success": True, "path": rel, "dirs": dirs, "files": files})
712
+
713
+
714
+ @app.route("/files/content/<path:key>")
715
+ @login_required
716
+ def file_content(key):
717
+ if not can_access_key(key):
718
+ return jsonify({"content": ""}), 403
719
+ file_rel = request.args.get("file", "") or ""
720
+ try:
721
+ full = safe_join_server_path(key, file_rel)
722
+ except Exception:
723
+ return jsonify({"content": ""}), 400
724
+ if os.path.isdir(full):
725
+ return jsonify({"content": ""}), 400
726
+ try:
727
+ with open(full, "r", encoding="utf-8", errors="ignore") as f:
728
+ return jsonify({"content": f.read()})
729
+ except Exception:
730
+ return jsonify({"content": ""})
731
+
732
+
733
+ @app.route("/files/save/<path:key>", methods=["POST"])
734
+ @login_required
735
+ def file_save(key):
736
+ if not can_access_key(key):
737
+ return jsonify({"success": False, "message": "Forbidden"}), 403
738
+
739
+ data = request.get_json(silent=True) or {}
740
+ file_rel = data.get("file", "") or ""
741
+ content = data.get("content", "")
742
+
743
+ try:
744
+ full = safe_join_server_path(key, file_rel)
745
+ except Exception:
746
+ return jsonify({"success": False, "message": "Invalid path"}), 400
747
+
748
+ os.makedirs(os.path.dirname(full), exist_ok=True)
749
+ try:
750
+ with open(full, "w", encoding="utf-8") as f:
751
+ f.write(content)
752
+ return jsonify({"success": True})
753
+ except Exception as e:
754
+ return jsonify({"success": False, "message": str(e)}), 500
755
+
756
+
757
+ @app.route("/files/mkdir/<path:key>", methods=["POST"])
758
+ @login_required
759
+ def file_mkdir(key):
760
+ if not can_access_key(key):
761
+ return jsonify({"success": False, "message": "Forbidden"}), 403
762
+ data = request.get_json(silent=True) or {}
763
+ rel = data.get("path", "") or ""
764
+ name = safe_name(data.get("name", ""))
765
+ if not name:
766
+ return jsonify({"success": False, "message": "Bad name"}), 400
767
+ try:
768
+ target = safe_join_server_path(key, os.path.join(rel, name))
769
+ os.makedirs(target, exist_ok=False)
770
+ return jsonify({"success": True})
771
+ except FileExistsError:
772
+ return jsonify({"success": False, "message": "Already exists"}), 409
773
+ except Exception as e:
774
+ return jsonify({"success": False, "message": str(e)}), 500
775
+
776
+
777
+ @app.route("/files/rename/<path:key>", methods=["POST"])
778
+ @login_required
779
+ def file_rename(key):
780
+ if not can_access_key(key):
781
+ return jsonify({"success": False, "message": "Forbidden"}), 403
782
+ data = request.get_json(silent=True) or {}
783
+ rel = data.get("path", "") or ""
784
+ old = safe_name(data.get("old", ""))
785
+ new = safe_name(data.get("new", ""))
786
+ if not old or not new:
787
+ return jsonify({"success": False, "message": "Bad name"}), 400
788
+ try:
789
+ src = safe_join_server_path(key, os.path.join(rel, old))
790
+ dst = safe_join_server_path(key, os.path.join(rel, new))
791
+ os.rename(src, dst)
792
+ return jsonify({"success": True})
793
+ except Exception as e:
794
+ return jsonify({"success": False, "message": str(e)}), 500
795
+
796
+
797
+ @app.route("/files/delete/<path:key>", methods=["POST"])
798
+ @login_required
799
+ def file_delete(key):
800
+ if not can_access_key(key):
801
+ return jsonify({"success": False, "message": "Forbidden"}), 403
802
+ data = request.get_json(silent=True) or {}
803
+ rel = data.get("path", "") or ""
804
+ name = safe_name(data.get("name", ""))
805
+ kind = (data.get("kind") or "file").lower()
806
+ if not name:
807
+ return jsonify({"success": False, "message": "Bad name"}), 400
808
+ try:
809
+ target = safe_join_server_path(key, os.path.join(rel, name))
810
+ if kind == "dir":
811
+ shutil.rmtree(target)
812
+ else:
813
+ os.remove(target)
814
+ return jsonify({"success": True})
815
+ except Exception as e:
816
+ return jsonify({"success": False, "message": str(e)}), 500
817
+
818
+
819
+ @app.route("/files/upload/<path:key>", methods=["POST"])
820
+ @login_required
821
+ def file_upload(key):
822
+ if not can_access_key(key):
823
+ return jsonify({"success": False, "message": "Forbidden"}), 403
824
+
825
+ rel = request.args.get("path", "") or ""
826
+ try:
827
+ base_dir = safe_join_server_path(key, rel)
828
+ except Exception:
829
+ return jsonify({"success": False, "message": "Invalid path"}), 400
830
+ os.makedirs(base_dir, exist_ok=True)
831
+
832
+ files = request.files.getlist("files") or []
833
+ if not files:
834
+ one = request.files.get("file")
835
+ if one:
836
+ files = [one]
837
+ if not files:
838
+ return jsonify({"success": False, "message": "No file"}), 400
839
+
840
+ relpaths = request.form.getlist("relpaths")
841
+ saved = 0
842
+
843
+ for i, f in enumerate(files):
844
+ if not f or not f.filename:
845
+ continue
846
+ filename = os.path.basename(f.filename)
847
+
848
+ rp = ""
849
+ if relpaths and i < len(relpaths):
850
+ rp = (relpaths[i] or "").replace("\\", "/").lstrip("/")
851
+
852
+ try:
853
+ if rp:
854
+ target_dir = safe_join_server_path(key, os.path.join(rel, os.path.dirname(rp)))
855
+ else:
856
+ target_dir = base_dir
857
+ except Exception:
858
+ continue
859
+
860
+ os.makedirs(target_dir, exist_ok=True)
861
+ f.save(os.path.join(target_dir, filename))
862
+ saved += 1
863
+
864
+ return jsonify({"success": True, "saved": saved})
865
+
866
+
867
+ @app.route("/api/admin/servers")
868
+ @admin_required
869
+ def admin_servers():
870
+ return jsonify({"success": True, "servers": list_all_servers_for_admin()})
871
+
872
+
873
+ @app.route("/api/admin/server/ban", methods=["POST"])
874
+ @admin_required
875
+ def admin_server_ban():
876
+ data = request.get_json(silent=True) or {}
877
+ key = (data.get("key") or "").strip()
878
+ banned = bool(data.get("banned", True))
879
+
880
+ owner, folder = parse_server_key(key, allow_admin=True)
881
+ server_dir = get_server_dir(owner, folder)
882
+ if not os.path.isdir(server_dir):
883
+ return jsonify({"success": False, "message": "Server not found"}), 404
884
+
885
+ meta = read_meta(owner, folder)
886
+ meta["banned"] = banned
887
+ write_meta(owner, folder, meta)
888
+
889
+ if banned:
890
+ stop_proc(key)
891
+ set_state(key, "Banned")
892
+ log_append(key, "[ADMIN] Server banned.\n")
893
+ else:
894
+ set_state(key, "Offline")
895
+ log_append(key, "[ADMIN] Server unbanned.\n")
896
+
897
+ return jsonify({"success": True})
898
+
899
+
900
+ @app.route("/api/admin/users")
901
+ @admin_required
902
+ def admin_users():
903
+ db = load_users()
904
+
905
+ counts = {}
906
+ if os.path.isdir(USERS_ROOT):
907
+ for owner in os.listdir(USERS_ROOT):
908
+ root = get_user_servers_root(owner)
909
+ if os.path.isdir(root):
910
+ counts[owner] = len([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))])
911
+
912
+ users = []
913
+ for u in db.get("users", []):
914
+ users.append({
915
+ "username": u.get("username"),
916
+ "email": u.get("email"),
917
+ "active": bool(u.get("active", True)),
918
+ "premium": bool(u.get("premium", False)),
919
+ "servers": counts.get(u.get("username") or "", 0),
920
+ })
921
+ return jsonify({"success": True, "users": users})
922
+
923
+
924
+ @app.route("/api/admin/user/update", methods=["POST"])
925
+ @admin_required
926
+ def admin_user_update():
927
+ data = request.get_json(silent=True) or {}
928
+ username = (data.get("username") or "").strip()
929
+ if not username:
930
+ return jsonify({"success": False, "message": "Username required"}), 400
931
+
932
+ db = load_users()
933
+ u = find_user(db, username)
934
+ if not u:
935
+ return jsonify({"success": False, "message": "User not found"}), 404
936
+
937
+ if "active" in data:
938
+ u["active"] = bool(data["active"])
939
+ if "premium" in data:
940
+ u["premium"] = bool(data["premium"])
941
+
942
+ save_users(db)
943
+ return jsonify({"success": True})
944
+
945
+
946
+ @app.route("/api/admin/quickstats")
947
+ @admin_required
948
+ def admin_quickstats():
949
+ total_servers = 0
950
+ running = 0
951
+ installing = 0
952
+ banned = 0
953
+
954
+ for s in list_all_servers_for_admin():
955
+ total_servers += 1
956
+ if s.get("status") == "Banned":
957
+ banned += 1
958
+ elif s.get("status") == "Running":
959
+ running += 1
960
+ elif s.get("status") in ("Installing", "Starting"):
961
+ installing += 1
962
+
963
+ db = load_users()
964
+ total_users = len(db.get("users", []))
965
+ active_users = sum(1 for u in db.get("users", []) if u.get("active", True))
966
+ premium_users = sum(1 for u in db.get("users", []) if u.get("premium", False))
967
+
968
+ return jsonify({"success": True, "stats": {
969
+ "servers_total": total_servers,
970
+ "servers_running": running,
971
+ "servers_installing": installing,
972
+ "servers_banned": banned,
973
+ "users_total": total_users,
974
+ "users_active": active_users,
975
+ "users_premium": premium_users
976
+ }})
977
+
978
+
979
+ if __name__ == "__main__":
980
+ port = int(os.environ.get("SERVER_PORT", 30170))
981
+ app.run(host="0.0.0.0", port=port)
create.html ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>الذئب الابيض · @j49_c</title>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
10
+
11
+ :root {
12
+ --bg: #000000;
13
+ --surface: rgba(28, 28, 30, 0.6);
14
+ --surface-border: rgba(255, 255, 255, 0.1);
15
+ --primary: #0A84FF;
16
+ --primary-hover: #0071E3;
17
+ --text: #F5F5F7;
18
+ --muted: #86868B;
19
+ --error: #FF453A;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ min-height: 100vh;
30
+ display: grid;
31
+ place-items: center;
32
+ color: var(--text);
33
+ background-color: var(--bg);
34
+ background-image:
35
+ radial-gradient(circle at 85% 50%, rgba(10, 132, 255, 0.15), transparent 25%),
36
+ radial-gradient(circle at 15% 30%, rgba(255, 255, 255, 0.05), transparent 25%);
37
+ overflow: hidden;
38
+ padding: 20px;
39
+ }
40
+
41
+ body::before {
42
+ content: '';
43
+ position: absolute;
44
+ top: 0;
45
+ left: 0;
46
+ right: 0;
47
+ height: 1px;
48
+ background: linear-gradient(90deg, transparent, rgba(10, 132, 255, 0.5), transparent);
49
+ }
50
+
51
+ .container {
52
+ position: relative;
53
+ z-index: 10;
54
+ width: 100%;
55
+ max-width: 440px;
56
+ animation: fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
57
+ opacity: 0;
58
+ transform: translateY(20px);
59
+ }
60
+
61
+ @keyframes fadeUp {
62
+ to {
63
+ opacity: 1;
64
+ transform: translateY(0);
65
+ }
66
+ }
67
+
68
+ .card {
69
+ background: var(--surface);
70
+ border: 1px solid var(--surface-border);
71
+ border-radius: 24px;
72
+ backdrop-filter: blur(40px);
73
+ -webkit-backdrop-filter: blur(40px);
74
+ padding: 40px 32px;
75
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.1);
76
+ }
77
+
78
+ .header {
79
+ text-align: center;
80
+ margin-bottom: 32px;
81
+ }
82
+
83
+ .logo {
84
+ font-size: 36px;
85
+ margin-bottom: 12px;
86
+ display: inline-block;
87
+ background: linear-gradient(135deg, #fff, #a1a1a6);
88
+ -webkit-background-clip: text;
89
+ -webkit-text-fill-color: transparent;
90
+ }
91
+
92
+ .title {
93
+ font-size: 22px;
94
+ font-weight: 700;
95
+ letter-spacing: -0.5px;
96
+ margin-bottom: 8px;
97
+ }
98
+
99
+ .sub {
100
+ color: var(--muted);
101
+ font-size: 13px;
102
+ font-weight: 400;
103
+ line-height: 1.5;
104
+ }
105
+
106
+ .input-group {
107
+ position: relative;
108
+ margin-bottom: 12px;
109
+ }
110
+
111
+ input {
112
+ width: 100%;
113
+ padding: 14px 16px;
114
+ border-radius: 12px;
115
+ border: 1px solid rgba(255, 255, 255, 0.12);
116
+ background: rgba(0, 0, 0, 0.3);
117
+ color: var(--text);
118
+ font-size: 15px;
119
+ outline: none;
120
+ transition: all 0.2s ease;
121
+ }
122
+
123
+ input::placeholder {
124
+ color: #6e6e73;
125
+ }
126
+
127
+ input:focus {
128
+ border-color: var(--primary);
129
+ background: rgba(0, 0, 0, 0.5);
130
+ box-shadow: 0 0 0 4px rgba(10, 132, 255, 0.15);
131
+ }
132
+
133
+ .actions {
134
+ display: flex;
135
+ gap: 12px;
136
+ margin-top: 24px;
137
+ flex-direction: column;
138
+ }
139
+
140
+ .btn {
141
+ width: 100%;
142
+ padding: 14px;
143
+ border-radius: 12px;
144
+ font-size: 15px;
145
+ font-weight: 600;
146
+ cursor: pointer;
147
+ transition: all 0.2s;
148
+ border: none;
149
+ outline: none;
150
+ text-align: center;
151
+ }
152
+
153
+ .btn-primary {
154
+ background: var(--primary);
155
+ color: white;
156
+ box-shadow: 0 4px 14px rgba(10, 132, 255, 0.3);
157
+ }
158
+
159
+ .btn-primary:hover {
160
+ background: var(--primary-hover);
161
+ transform: translateY(-1px);
162
+ }
163
+
164
+ .btn-primary:active {
165
+ transform: translateY(1px);
166
+ }
167
+
168
+ .btn-secondary {
169
+ background: transparent;
170
+ color: var(--primary);
171
+ border: 1px solid transparent;
172
+ }
173
+
174
+ .btn-secondary:hover {
175
+ background: rgba(10, 132, 255, 0.1);
176
+ }
177
+
178
+ .msg {
179
+ margin-top: 16px;
180
+ color: var(--muted);
181
+ font-size: 13px;
182
+ text-align: center;
183
+ min-height: 20px;
184
+ }
185
+
186
+ .footer {
187
+ text-align: center;
188
+ margin-top: 24px;
189
+ font-size: 12px;
190
+ color: #424245;
191
+ }
192
+ </style>
193
+ </head>
194
+
195
+ <body>
196
+ <div class="container">
197
+ <div class="card">
198
+ <div class="header">
199
+ <div class="logo">الذئب الابيض</div>
200
+ <div class="title">Clearance Request</div>
201
+ <div class="sub">Register your operative credentials.<br>System limited to isolated container environments.
202
+ </div>
203
+ </div>
204
+
205
+ <div class="input-group">
206
+ <input id="u" placeholder="Agent Designation (e.g., neo_01)">
207
+ </div>
208
+ <div class="input-group">
209
+ <input id="e" placeholder="Secure Comm (Email)">
210
+ </div>
211
+ <div class="input-group">
212
+ <input id="p" placeholder="Passcode (min 6 chars)" type="password">
213
+ </div>
214
+ <div class="input-group">
215
+ <input id="p2" placeholder="Verify Passcode" type="password">
216
+ </div>
217
+
218
+ <div class="actions">
219
+ <button class="btn btn-primary" onclick="doCreate()">Submit Request</button>
220
+ <button class="btn btn-secondary" onclick="location.href='/login'">Return to Authentication</button>
221
+ </div>
222
+
223
+ <div class="msg" id="m"></div>
224
+ </div>
225
+ <div class="footer">All rights reserved. الذئب الابيض - @j49_c</div>
226
+ </div>
227
+
228
+ <script>
229
+ async function doCreate() {
230
+ const m = document.getElementById('m');
231
+ m.style.color = "var(--muted)";
232
+ m.textContent = 'Processing clearance request...';
233
+ const username = document.getElementById('u').value.trim();
234
+ const email = document.getElementById('e').value.trim();
235
+ const password = document.getElementById('p').value;
236
+ const password2 = document.getElementById('p2').value;
237
+
238
+ const r = await fetch('/api/auth/create', {
239
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({ username, email, password, password2 })
241
+ });
242
+ const d = await r.json().catch(() => ({ success: false, message: 'Server transmission error' }));
243
+ if (!r.ok || !d.success) { m.style.color = "var(--error)"; m.textContent = d.message || 'Clearance denied'; return; }
244
+
245
+ m.style.color = "#32D74B";
246
+ m.textContent = 'Clearance granted. Redirecting...';
247
+ setTimeout(() => location.href = '/login', 1200);
248
+ }
249
+ </script>
250
+ </body>
251
+
252
+ </html>
index.html ADDED
@@ -0,0 +1,1159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>الذئب الابيض · @j49_c</title>
8
+
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
11
+
12
+ :root {
13
+ --bg: #000000;
14
+ --bg-elevated: rgba(28, 28, 30, 0.45);
15
+ --bg-card: rgba(44, 44, 46, 0.2);
16
+ --border: rgba(255, 255, 255, 0.12);
17
+ --text: #F5F5F7;
18
+ --muted: #86868B;
19
+ --primary: #0A84FF;
20
+ --primary-hover: #0071E3;
21
+ --danger: #FF453A;
22
+ --ok: #32D74B;
23
+ --warn: #FF9F0A;
24
+ --shadow: 0 16px 48px rgba(0, 0, 0, 0.8);
25
+ --radius: 20px;
26
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
27
+ }
28
+
29
+ * {
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ html,
34
+ body {
35
+ height: 100%;
36
+ margin: 0;
37
+ }
38
+
39
+ body {
40
+ color: var(--text);
41
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
42
+ background-color: var(--bg);
43
+ background-image:
44
+ radial-gradient(1000px 500px at 15% 10%, rgba(10, 132, 255, 0.12), transparent 40%),
45
+ radial-gradient(1000px 500px at 85% 90%, rgba(255, 255, 255, 0.05), transparent 40%);
46
+ background-attachment: fixed;
47
+ overflow-x: hidden;
48
+ }
49
+
50
+ body::after {
51
+ content: '';
52
+ position: fixed;
53
+ inset: 0;
54
+ pointer-events: none;
55
+ z-index: -1;
56
+ background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255, 255, 255, 0.01) 2px, rgba(255, 255, 255, 0.01) 4px);
57
+ opacity: 0.3;
58
+ }
59
+
60
+ button {
61
+ font-family: inherit;
62
+ }
63
+
64
+ .topbar {
65
+ position: sticky;
66
+ top: 0;
67
+ z-index: 40;
68
+ background: rgba(0, 0, 0, 0.6);
69
+ backdrop-filter: blur(24px);
70
+ -webkit-backdrop-filter: blur(24px);
71
+ border-bottom: 1px solid var(--border);
72
+ }
73
+
74
+ .topbar-inner {
75
+ max-width: 1200px;
76
+ margin: 0 auto;
77
+ padding: 12px 24px;
78
+ display: flex;
79
+ gap: 16px;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ }
83
+
84
+ .brand {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 16px;
88
+ cursor: pointer;
89
+ }
90
+
91
+ .logo {
92
+ font-size: 28px;
93
+ width: 44px;
94
+ height: 44px;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
99
+ border: 1px solid rgba(255, 255, 255, 0.2);
100
+ border-radius: 12px;
101
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
102
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
103
+ }
104
+
105
+ .title-box {
106
+ display: flex;
107
+ flex-direction: column;
108
+ justify-content: center;
109
+ }
110
+
111
+ .title {
112
+ font-weight: 700;
113
+ font-size: 16px;
114
+ letter-spacing: -0.2px;
115
+ }
116
+
117
+ .sub {
118
+ font-size: 12px;
119
+ color: var(--muted);
120
+ font-weight: 500;
121
+ font-family: var(--mono);
122
+ }
123
+
124
+ .actions {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 12px;
128
+ flex-wrap: wrap;
129
+ }
130
+
131
+ .btn {
132
+ padding: 10px 16px;
133
+ border-radius: 12px;
134
+ font-size: 14px;
135
+ font-weight: 600;
136
+ text-align: center;
137
+ cursor: pointer;
138
+ transition: all 0.2s;
139
+ outline: none;
140
+ background: var(--bg-card);
141
+ color: var(--text);
142
+ border: 1px solid var(--border);
143
+ backdrop-filter: blur(10px);
144
+ -webkit-backdrop-filter: blur(10px);
145
+ }
146
+
147
+ .btn:hover {
148
+ background: rgba(255, 255, 255, 0.12);
149
+ border-color: rgba(255, 255, 255, 0.2);
150
+ transform: translateY(-1px);
151
+ }
152
+
153
+ .btn:active {
154
+ transform: translateY(1px);
155
+ }
156
+
157
+ .btn-primary {
158
+ background: var(--primary);
159
+ color: white;
160
+ border: none;
161
+ box-shadow: 0 4px 14px rgba(10, 132, 255, 0.3);
162
+ }
163
+
164
+ .btn-primary:hover {
165
+ background: var(--primary-hover);
166
+ border: none;
167
+ }
168
+
169
+ .btn-danger {
170
+ background: rgba(255, 69, 58, 0.15);
171
+ color: var(--danger);
172
+ border-color: rgba(255, 69, 58, 0.3);
173
+ }
174
+
175
+ .btn-danger:hover {
176
+ background: rgba(255, 69, 58, 0.25);
177
+ border-color: rgba(255, 69, 58, 0.5);
178
+ }
179
+
180
+ .btn-mini {
181
+ padding: 6px 12px;
182
+ font-size: 12px;
183
+ border-radius: 8px;
184
+ }
185
+
186
+ .container {
187
+ max-width: 1200px;
188
+ margin: 32px auto 60px;
189
+ padding: 0 24px;
190
+ animation: fadeUp 0.6s ease;
191
+ }
192
+
193
+ @keyframes fadeUp {
194
+ from {
195
+ opacity: 0;
196
+ transform: translateY(10px);
197
+ }
198
+
199
+ to {
200
+ opacity: 1;
201
+ transform: translateY(0);
202
+ }
203
+ }
204
+
205
+ .grid {
206
+ display: grid;
207
+ grid-template-columns: repeat(4, 1fr);
208
+ gap: 16px;
209
+ }
210
+
211
+ @media (max-width: 1050px) {
212
+ .grid {
213
+ grid-template-columns: repeat(3, 1fr);
214
+ }
215
+ }
216
+
217
+ @media (max-width: 760px) {
218
+ .grid {
219
+ grid-template-columns: repeat(2, 1fr);
220
+ }
221
+ }
222
+
223
+ @media (max-width: 480px) {
224
+ .grid {
225
+ grid-template-columns: 1fr;
226
+ }
227
+ }
228
+
229
+ .card {
230
+ background: var(--bg-elevated);
231
+ border: 1px solid var(--border);
232
+ border-radius: var(--radius);
233
+ padding: 20px;
234
+ box-shadow: var(--shadow);
235
+ backdrop-filter: blur(20px);
236
+ -webkit-backdrop-filter: blur(20px);
237
+ cursor: pointer;
238
+ transition: all 0.2s;
239
+ position: relative;
240
+ overflow: hidden;
241
+ min-height: 140px;
242
+ display: flex;
243
+ flex-direction: column;
244
+ justify-content: space-between;
245
+ }
246
+
247
+ .card:hover {
248
+ transform: translateY(-4px);
249
+ border-color: var(--primary);
250
+ box-shadow: 0 20px 60px rgba(10, 132, 255, 0.15);
251
+ }
252
+
253
+ .card::before {
254
+ content: '';
255
+ position: absolute;
256
+ inset: 0;
257
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), transparent);
258
+ pointer-events: none;
259
+ }
260
+
261
+ .srv-title {
262
+ font-weight: 700;
263
+ font-size: 16px;
264
+ letter-spacing: -0.3px;
265
+ margin: 0 0 4px;
266
+ word-break: break-all;
267
+ }
268
+
269
+ .srv-sub {
270
+ color: var(--muted);
271
+ font-size: 12px;
272
+ line-height: 1.4;
273
+ display: -webkit-box;
274
+ -webkit-line-clamp: 2;
275
+ -webkit-box-orient: vertical;
276
+ overflow: hidden;
277
+ }
278
+
279
+ .pill {
280
+ display: inline-flex;
281
+ align-items: center;
282
+ gap: 8px;
283
+ padding: 6px 12px;
284
+ border-radius: 99px;
285
+ background: rgba(0, 0, 0, 0.5);
286
+ border: 1px solid var(--border);
287
+ font-size: 11px;
288
+ font-weight: 700;
289
+ letter-spacing: 0.5px;
290
+ text-transform: uppercase;
291
+ margin-top: 16px;
292
+ width: fit-content;
293
+ }
294
+
295
+ .dot {
296
+ width: 8px;
297
+ height: 8px;
298
+ border-radius: 50%;
299
+ }
300
+
301
+ .pill.running .dot {
302
+ background: var(--ok);
303
+ box-shadow: 0 0 8px var(--ok);
304
+ }
305
+
306
+ .pill.installing .dot {
307
+ background: var(--warn);
308
+ box-shadow: 0 0 8px var(--warn);
309
+ }
310
+
311
+ .pill.banned .dot {
312
+ background: var(--danger);
313
+ box-shadow: 0 0 8px var(--danger);
314
+ }
315
+
316
+ .pill.offline .dot {
317
+ background: var(--muted);
318
+ }
319
+
320
+ #serverView {
321
+ display: none;
322
+ }
323
+
324
+ .header-row {
325
+ display: flex;
326
+ align-items: flex-end;
327
+ justify-content: space-between;
328
+ gap: 16px;
329
+ flex-wrap: wrap;
330
+ margin-bottom: 24px;
331
+ }
332
+
333
+ .hleft h1 {
334
+ margin: 0 0 4px;
335
+ font-size: 28px;
336
+ font-weight: 700;
337
+ letter-spacing: -0.8px;
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 12px;
341
+ }
342
+
343
+ .hleft h1::before {
344
+ content: '>';
345
+ color: var(--primary);
346
+ font-family: var(--mono);
347
+ font-weight: 400;
348
+ }
349
+
350
+ .hleft .muted {
351
+ color: var(--muted);
352
+ font-size: 14px;
353
+ font-family: var(--mono);
354
+ }
355
+
356
+ .tabs {
357
+ display: flex;
358
+ gap: 8px;
359
+ background: rgba(0, 0, 0, 0.3);
360
+ padding: 4px;
361
+ border-radius: 14px;
362
+ border: 1px solid var(--border);
363
+ }
364
+
365
+ .tab {
366
+ background: transparent;
367
+ border: none;
368
+ color: var(--muted);
369
+ padding: 8px 16px;
370
+ border-radius: 10px;
371
+ font-size: 13px;
372
+ font-weight: 600;
373
+ cursor: pointer;
374
+ transition: all 0.2s;
375
+ outline: none;
376
+ }
377
+
378
+ .tab:hover {
379
+ color: var(--text);
380
+ }
381
+
382
+ .tab.active {
383
+ background: rgba(255, 255, 255, 0.1);
384
+ color: var(--text);
385
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
386
+ }
387
+
388
+ .panel {
389
+ border: 1px solid var(--border);
390
+ border-radius: var(--radius);
391
+ background: var(--bg-elevated);
392
+ backdrop-filter: blur(20px);
393
+ -webkit-backdrop-filter: blur(20px);
394
+ box-shadow: var(--shadow);
395
+ overflow: hidden;
396
+ }
397
+
398
+ .grid-2 {
399
+ display: grid;
400
+ grid-template-columns: 1fr 340px;
401
+ }
402
+
403
+ @media (max-width: 980px) {
404
+ .grid-2 {
405
+ grid-template-columns: 1fr;
406
+ }
407
+ }
408
+
409
+ .console-wrapper {
410
+ padding: 20px;
411
+ border-right: 1px solid var(--border);
412
+ background: rgba(0, 0, 0, 0.2);
413
+ }
414
+
415
+ @media (max-width: 980px) {
416
+ .console-wrapper {
417
+ border-right: none;
418
+ border-bottom: 1px solid var(--border);
419
+ }
420
+ }
421
+
422
+ .console-header {
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 8px;
426
+ margin-bottom: 12px;
427
+ font-family: var(--mono);
428
+ font-size: 12px;
429
+ color: var(--muted);
430
+ }
431
+
432
+ .console-header::before {
433
+ content: '';
434
+ display: inline-block;
435
+ width: 8px;
436
+ height: 8px;
437
+ border-radius: 50%;
438
+ background: #ff5f56;
439
+ box-shadow: 14px 0 0 #ffbd2e, 28px 0 0 #27c93f;
440
+ margin-right: 28px;
441
+ }
442
+
443
+ .console {
444
+ background: transparent;
445
+ height: 600px;
446
+ overflow-y: auto;
447
+ overflow-x: auto;
448
+ font-family: var(--mono);
449
+ font-size: 13px;
450
+ line-height: 1.6;
451
+ color: rgba(255, 255, 255, 0.85);
452
+ white-space: pre-wrap;
453
+ scrollbar-width: thin;
454
+ scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
455
+ }
456
+
457
+ .console::-webkit-scrollbar {
458
+ width: 8px;
459
+ height: 8px;
460
+ }
461
+
462
+ .console::-webkit-scrollbar-thumb {
463
+ background: rgba(255, 255, 255, 0.2);
464
+ border-radius: 4px;
465
+ }
466
+
467
+ .side {
468
+ padding: 20px;
469
+ display: flex;
470
+ flex-direction: column;
471
+ gap: 12px;
472
+ background: rgba(255, 255, 255, 0.02);
473
+ }
474
+
475
+ .btn-block {
476
+ width: 100%;
477
+ padding: 14px;
478
+ font-size: 14px;
479
+ }
480
+
481
+ .stat {
482
+ border: 1px solid var(--border);
483
+ background: rgba(0, 0, 0, 0.3);
484
+ border-radius: 16px;
485
+ padding: 16px;
486
+ display: flex;
487
+ gap: 14px;
488
+ align-items: center;
489
+ }
490
+
491
+ .stat .ico {
492
+ width: 40px;
493
+ height: 40px;
494
+ border-radius: 12px;
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: center;
498
+ background: rgba(255, 255, 255, 0.05);
499
+ border: 1px solid rgba(255, 255, 255, 0.1);
500
+ color: var(--primary);
501
+ font-size: 20px;
502
+ }
503
+
504
+ .stat-content {
505
+ display: flex;
506
+ flex-direction: column;
507
+ gap: 4px;
508
+ }
509
+
510
+ .stat small {
511
+ color: var(--muted);
512
+ font-size: 11px;
513
+ font-weight: 600;
514
+ text-transform: uppercase;
515
+ letter-spacing: 0.5px;
516
+ }
517
+
518
+ .stat b {
519
+ font-size: 14px;
520
+ font-family: var(--mono);
521
+ font-weight: 500;
522
+ word-break: break-all;
523
+ }
524
+
525
+ #mainFileTxt {
526
+ color: var(--primary);
527
+ cursor: pointer;
528
+ border-bottom: 1px dashed rgba(10, 132, 255, 0.5);
529
+ }
530
+
531
+ #mainFileTxt:hover {
532
+ color: var(--primary-hover);
533
+ }
534
+
535
+ .fm-top {
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: space-between;
539
+ gap: 16px;
540
+ flex-wrap: wrap;
541
+ padding: 20px;
542
+ border-bottom: 1px solid var(--border);
543
+ background: rgba(0, 0, 0, 0.2);
544
+ }
545
+
546
+ .path-container {
547
+ display: flex;
548
+ flex-direction: column;
549
+ gap: 8px;
550
+ }
551
+
552
+ .path-label {
553
+ font-weight: 600;
554
+ font-size: 16px;
555
+ }
556
+
557
+ .path {
558
+ font-family: var(--mono);
559
+ font-size: 13px;
560
+ color: var(--muted);
561
+ display: flex;
562
+ align-items: center;
563
+ gap: 6px;
564
+ }
565
+
566
+ .crumb a {
567
+ color: var(--primary);
568
+ text-decoration: none;
569
+ padding: 2px 6px;
570
+ border-radius: 6px;
571
+ }
572
+
573
+ .crumb a:hover {
574
+ background: rgba(10, 132, 255, 0.1);
575
+ }
576
+
577
+ .crumb .sep {
578
+ color: rgba(255, 255, 255, 0.2);
579
+ }
580
+
581
+ .table-wrapper {
582
+ overflow-x: auto;
583
+ }
584
+
585
+ .table {
586
+ width: 100%;
587
+ border-collapse: collapse;
588
+ text-align: left;
589
+ }
590
+
591
+ .table th,
592
+ .table td {
593
+ padding: 14px 20px;
594
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
595
+ font-size: 14px;
596
+ }
597
+
598
+ .table th {
599
+ color: var(--muted);
600
+ font-size: 12px;
601
+ font-weight: 500;
602
+ background: rgba(0, 0, 0, 0.3);
603
+ }
604
+
605
+ .table tr:hover td {
606
+ background: rgba(255, 255, 255, 0.02);
607
+ }
608
+
609
+ .table td:last-child {
610
+ width: 220px;
611
+ }
612
+
613
+ .name-link {
614
+ font-weight: 500;
615
+ cursor: pointer;
616
+ display: flex;
617
+ align-items: center;
618
+ gap: 8px;
619
+ }
620
+
621
+ .name-link:hover {
622
+ color: var(--primary);
623
+ }
624
+
625
+ .modal-backdrop {
626
+ position: fixed;
627
+ inset: 0;
628
+ background: rgba(0, 0, 0, 0.8);
629
+ backdrop-filter: blur(10px);
630
+ -webkit-backdrop-filter: blur(10px);
631
+ display: none;
632
+ align-items: center;
633
+ justify-content: center;
634
+ z-index: 100;
635
+ padding: 20px;
636
+ }
637
+
638
+ .modal {
639
+ width: 100%;
640
+ max-width: 420px;
641
+ background: rgba(28, 28, 30, 0.8);
642
+ border: 1px solid var(--border);
643
+ border-radius: 24px;
644
+ box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6);
645
+ padding: 32px;
646
+ animation: fadeUp 0.3s ease;
647
+ }
648
+
649
+ .modal h3 {
650
+ margin: 0 0 16px 0;
651
+ font-size: 20px;
652
+ font-weight: 600;
653
+ letter-spacing: -0.5px;
654
+ }
655
+
656
+ .modal input {
657
+ width: 100%;
658
+ padding: 14px 16px;
659
+ border-radius: 12px;
660
+ border: 1px solid var(--border);
661
+ background: rgba(0, 0, 0, 0.4);
662
+ color: var(--text);
663
+ outline: none;
664
+ font-size: 15px;
665
+ margin-bottom: 24px;
666
+ }
667
+
668
+ .modal input:focus {
669
+ border-color: var(--primary);
670
+ }
671
+
672
+ .modal-actions {
673
+ display: flex;
674
+ justify-content: flex-end;
675
+ gap: 12px;
676
+ }
677
+
678
+ #editor-modal {
679
+ display: none;
680
+ position: fixed;
681
+ inset: 0;
682
+ background: var(--bg);
683
+ z-index: 120;
684
+ }
685
+
686
+ .editor {
687
+ height: 100%;
688
+ display: flex;
689
+ flex-direction: column;
690
+ }
691
+
692
+ .editor-top {
693
+ padding: 16px 24px;
694
+ border-bottom: 1px solid var(--border);
695
+ background: rgba(28, 28, 30, 0.6);
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: space-between;
699
+ gap: 16px;
700
+ }
701
+
702
+ .editor-top .fn {
703
+ font-family: var(--mono);
704
+ font-size: 14px;
705
+ font-weight: 500;
706
+ color: var(--text);
707
+ }
708
+
709
+ #ed-area {
710
+ flex: 1;
711
+ background: #1e1e1e;
712
+ border: none;
713
+ padding: 24px;
714
+ outline: none;
715
+ color: #d4d4d4;
716
+ font-family: var(--mono);
717
+ font-size: 14px;
718
+ line-height: 1.6;
719
+ resize: none;
720
+ }
721
+ </style>
722
+ </head>
723
+
724
+ <body>
725
+ <div class="topbar">
726
+ <div class="topbar-inner">
727
+ <div class="brand" onclick="goHome()" role="button" tabindex="0">
728
+ <div class="logo"></div>
729
+ <div class="title-box">
730
+ <div class="title">الذئب الابيض</div>
731
+ <span class="sub" id="pyHostTxt">Connecting...</span>
732
+ </div>
733
+ </div>
734
+
735
+ <div class="actions">
736
+ <button id="adminBtn" class="btn" style="display:none" onclick="location.href='/admin'">HQ Interface</button>
737
+ <button id="homeBtn" class="btn" style="display:none" onclick="goHome()">Directory</button>
738
+ <button id="newBtn" class="btn btn-primary">+ Initialize Node</button>
739
+ <button class="btn btn-danger" onclick="location.href='/logout'">Disconnect</button>
740
+ </div>
741
+ </div>
742
+ </div>
743
+
744
+ <div class="container" id="homeView">
745
+ <div class="grid" id="cardsContainer"></div>
746
+ </div>
747
+
748
+ <div class="container" id="serverView">
749
+ <div class="header-row">
750
+ <div class="hleft">
751
+ <h1 id="srvHeader">NODE</h1>
752
+ <div class="muted" id="srvSub">Container Management Protocol</div>
753
+ </div>
754
+ <div class="tabs">
755
+ <button class="tab active" id="tabConsole" onclick="switchTab('console')">Terminal Output</button>
756
+ <button class="tab" id="tabFiles" onclick="switchTab('files')">System Storage</button>
757
+ </div>
758
+ </div>
759
+
760
+ <div class="panel" id="viewConsole">
761
+ <div class="grid-2">
762
+ <div class="console-wrapper">
763
+ <div class="console-header">bash — terminal</div>
764
+ <div class="console" id="logOut">Initializing secure connection...</div>
765
+ </div>
766
+
767
+ <div class="side">
768
+ <button id="btnStart" class="btn btn-block btn-primary" onclick="srvAction('start')">EXECUTE</button>
769
+ <button class="btn btn-block" onclick="srvAction('restart')">REBOOT</button>
770
+ <button class="btn btn-block btn-danger" onclick="srvAction('stop')">HALT</button>
771
+
772
+ <div style="height: 12px;"></div>
773
+
774
+ <div class="stat">
775
+ <div class="ico">
776
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
777
+ stroke-linejoin="round">
778
+ <path d="M13 10V3L4 14h7v7l9-11h-7z" />
779
+ </svg>
780
+ </div>
781
+ <div class="stat-content"><small>STATE</small><b id="statusTxt">Offline</b></div>
782
+ </div>
783
+ <div class="stat">
784
+ <div class="ico">
785
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
786
+ stroke-linejoin="round">
787
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
788
+ <path d="M9 9h6v6H9z" />
789
+ <path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3" />
790
+ </svg>
791
+ </div>
792
+ <div class="stat-content"><small>MEMORY</small><b id="memTxt">0 MB</b></div>
793
+ </div>
794
+ <div class="stat">
795
+ <div class="ico">
796
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
797
+ stroke-linejoin="round">
798
+ <path d="M4 17L10 11 4 5" />
799
+ <path d="M12 19H20" />
800
+ </svg>
801
+ </div>
802
+ <div class="stat-content"><small>ENTRYPOINT</small><b id="mainFileTxt" onclick="changeStartup()">Click to
803
+ assign</b></div>
804
+ </div>
805
+ <div class="stat">
806
+ <div class="ico">
807
+ <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
808
+ stroke-linejoin="round">
809
+ <circle cx="12" cy="12" r="10" />
810
+ <path d="M2 12h20" />
811
+ <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
812
+ </svg>
813
+ </div>
814
+ <div class="stat-content"><small>ADDRESS</small><b id="ipTxt">0.0.0.0</b></div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ </div>
819
+
820
+ <div class="panel" id="viewFiles" style="display:none">
821
+ <div class="fm-top">
822
+ <div class="path-container">
823
+ <div class="path-label">Storage Volume</div>
824
+ <div class="path" id="pathBar"></div>
825
+ </div>
826
+ <div class="actions">
827
+ <button id="btnBack" class="btn btn-mini" onclick="goUp()" style="display:none">⬅ Return</button>
828
+ <button class="btn btn-mini" onclick="newFolder()">+ Directory</button>
829
+ <button class="btn btn-mini btn-primary" onclick="document.getElementById('uplFiles').click()">Upload
830
+ File</button>
831
+ <button class="btn btn-mini" onclick="document.getElementById('uplFolder').click()">Upload Bundle</button>
832
+ </div>
833
+ </div>
834
+
835
+ <input type="file" id="uplFiles" multiple hidden onchange="doUploadFiles(this)">
836
+ <input type="file" id="uplFolder" webkitdirectory directory multiple hidden onchange="doUploadFolder(this)">
837
+
838
+ <div class="table-wrapper">
839
+ <table class="table">
840
+ <thead>
841
+ <tr>
842
+ <th>Entity</th>
843
+ <th>Size</th>
844
+ <th>Protocols</th>
845
+ </tr>
846
+ </thead>
847
+ <tbody id="fileList"></tbody>
848
+ </table>
849
+ </div>
850
+ </div>
851
+ </div>
852
+
853
+ <div id="editor-modal">
854
+ <div class="editor">
855
+ <div class="editor-top">
856
+ <div class="fn" id="ed-title">file</div>
857
+ <div class="actions">
858
+ <button class="btn btn-primary" onclick="saveFile()">Write to Disk</button>
859
+ <button class="btn" onclick="closeEditor()">Close Editor</button>
860
+ </div>
861
+ </div>
862
+ <textarea id="ed-area" spellcheck="false"></textarea>
863
+ </div>
864
+ </div>
865
+
866
+ <div id="modalBackdrop" class="modal-backdrop">
867
+ <div class="modal">
868
+ <h3 id="mTitle">Title</h3>
869
+ <input id="mIn" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
870
+ <div class="modal-actions">
871
+ <button class="btn" onclick="closeM()">Abort</button>
872
+ <button id="mConfirm" class="btn btn-primary">Proceed</button>
873
+ </div>
874
+ </div>
875
+ </div>
876
+
877
+ <script>
878
+ let currentSrv = null, poll = null, currentFile = null, currentPath = '';
879
+
880
+ function isAuthPage() { return location.pathname.startsWith('/login') || location.pathname.startsWith('/create'); }
881
+ function safeRedirect(url) { if (!isAuthPage()) location.href = url; }
882
+
883
+ function statusClass(st) {
884
+ st = (st || '').toLowerCase();
885
+ if (st === 'running') return 'running';
886
+ if (st === 'installing' || st === 'starting') return 'installing';
887
+ if (st === 'banned') return 'banned';
888
+ return 'offline';
889
+ }
890
+
891
+ async function loadPythonHost() {
892
+ const ipTxt = document.getElementById('pyHostTxt');
893
+ try {
894
+ const r = await fetch('/servers');
895
+ if (r.redirected) { safeRedirect(r.url); return; }
896
+ const d = await r.json();
897
+ const port = location.port || 5000;
898
+ if (!d.servers || d.servers.length === 0) { ipTxt.textContent = `sys.addr: ${location.hostname}:${port}`; return; }
899
+ const key = d.servers[0].key || d.servers[0].folder;
900
+ const s = await fetch(`/server/stats/${encodeURIComponent(key)}`);
901
+ if (s.redirected) { safeRedirect(s.url); return; }
902
+ const sd = await s.json();
903
+ const ip = sd.ip || location.hostname;
904
+ ipTxt.textContent = `sys.addr: ${ip}:${port}`;
905
+ } catch (e) {
906
+ const port = location.port || 5000;
907
+ ipTxt.textContent = `sys.addr: ${location.hostname}:${port}`;
908
+ }
909
+ }
910
+
911
+ async function loadServers() {
912
+ const r = await fetch('/servers');
913
+ if (r.redirected) { safeRedirect(r.url); return; }
914
+ const d = await r.json();
915
+ const cont = document.getElementById('cardsContainer');
916
+ cont.innerHTML = '';
917
+ fetch('/admin').then(res => { if (!res.redirected && res.status === 200) document.getElementById('adminBtn').style.display = 'inline-block'; }).catch(() => { });
918
+
919
+ (d.servers || []).forEach(s => {
920
+ s.key = s.key || s.folder;
921
+ const div = document.createElement('div');
922
+ div.className = 'card';
923
+ const cls = statusClass(s.status);
924
+ const label = (s.status || 'Offline').toUpperCase();
925
+
926
+ div.innerHTML = `
927
+ <div>
928
+ <div class="srv-title">${escapeHtml(s.title)}</div>
929
+ <div class="srv-sub">${escapeHtml(s.subtitle || '')}</div>
930
+ </div>
931
+ <div class="pill ${cls}"><span class="dot"></span>${label}</div>
932
+ `;
933
+ div.onclick = () => openServer(s);
934
+ cont.appendChild(div);
935
+ });
936
+ }
937
+
938
+ function openServer(s) {
939
+ currentSrv = s; currentPath = '';
940
+ document.getElementById('homeView').style.display = 'none';
941
+ document.getElementById('serverView').style.display = 'block';
942
+ document.getElementById('homeBtn').style.display = 'inline-block';
943
+ document.getElementById('newBtn').style.display = 'none';
944
+
945
+ document.getElementById('srvHeader').textContent = s.title;
946
+ document.getElementById('srvSub').textContent = s.subtitle || 'Container node verified';
947
+ document.getElementById('mainFileTxt').textContent = s.startup_file || "Not assigned";
948
+ switchTab('console');
949
+ }
950
+
951
+ function goHome() {
952
+ document.getElementById('homeView').style.display = 'block';
953
+ document.getElementById('serverView').style.display = 'none';
954
+ document.getElementById('homeBtn').style.display = 'none';
955
+ document.getElementById('newBtn').style.display = 'inline-block';
956
+ clearInterval(poll); loadServers();
957
+ }
958
+
959
+ function switchTab(t) {
960
+ document.getElementById('tabConsole').classList.toggle('active', t === 'console');
961
+ document.getElementById('tabFiles').classList.toggle('active', t === 'files');
962
+ document.getElementById('viewConsole').style.display = t === 'console' ? 'block' : 'none';
963
+ document.getElementById('viewFiles').style.display = t === 'files' ? 'block' : 'none';
964
+
965
+ if (t === 'console') startPolling();
966
+ else { clearInterval(poll); loadFiles(); }
967
+ }
968
+
969
+ async function startPolling() {
970
+ if (poll) clearInterval(poll);
971
+ const fn = async () => {
972
+ try {
973
+ const r = await fetch(`/server/stats/${encodeURIComponent(currentSrv.key)}`);
974
+ if (r.redirected) { safeRedirect(r.url); return; }
975
+ const d = await r.json();
976
+
977
+ document.getElementById('logOut').textContent = d.logs || "Connection established. Awaiting stream...";
978
+ document.getElementById('statusTxt').textContent = d.status || 'Offline';
979
+ document.getElementById('memTxt').textContent = d.mem || '0 MB';
980
+ document.getElementById('ipTxt').textContent = d.ip || '';
981
+
982
+ const running = (d.status === "Running" || d.status === "Installing" || d.status === "Starting");
983
+ document.getElementById('btnStart').disabled = running;
984
+ if (running) document.getElementById('btnStart').style.opacity = 0.5;
985
+ else document.getElementById('btnStart').style.opacity = 1;
986
+
987
+ const lo = document.getElementById('logOut');
988
+ lo.scrollTop = lo.scrollHeight;
989
+ } catch (e) { }
990
+ };
991
+ fn(); poll = setInterval(fn, 2000);
992
+ }
993
+
994
+ async function srvAction(act) { await fetch(`/server/action/${encodeURIComponent(currentSrv.key)}/${act}`, { method: 'POST' }); }
995
+
996
+ function changeStartup() {
997
+ openM("Assign Entrypoint (e.g., bot.py)", currentSrv.startup_file || "", async (v) => {
998
+ await fetch(`/server/set-startup/${encodeURIComponent(currentSrv.key)}`, {
999
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: v })
1000
+ });
1001
+ currentSrv.startup_file = v;
1002
+ document.getElementById('mainFileTxt').textContent = v || "Not assigned";
1003
+ closeM();
1004
+ });
1005
+ }
1006
+
1007
+ async function loadFiles() {
1008
+ const r = await fetch(`/files/list/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`);
1009
+ if (r.redirected) { safeRedirect(r.url); return; }
1010
+ const data = await r.json();
1011
+ if (!data.success) { currentPath = ''; return loadFiles(); }
1012
+
1013
+ document.getElementById("btnBack").style.display = (currentPath && currentPath.length > 0) ? "inline-block" : "none";
1014
+
1015
+ const parts = (data.path || '').split('/').filter(Boolean);
1016
+ let acc = '';
1017
+ const crumbs = [`<span class="crumb"><a href="javascript:void(0)" onclick="jumpPath('')">~</a></span>`];
1018
+ for (const p of parts) {
1019
+ acc = acc ? (acc + '/' + p) : p;
1020
+ crumbs.push(`<span class="sep">/</span><span class="crumb"><a href="javascript:void(0)" onclick="jumpPath('${escapeAttr(acc)}')">${escapeHtml(p)}</a></span>`);
1021
+ }
1022
+ document.getElementById('pathBar').innerHTML = crumbs.join('');
1023
+
1024
+ const rows = [];
1025
+ for (const d of (data.dirs || [])) {
1026
+ const name = d.name;
1027
+ rows.push(`
1028
+ <tr>
1029
+ <td><span class="name-link" onclick="openFolder('${escapeAttr(name)}')">📁 ${escapeHtml(name)}</span></td>
1030
+ <td style="color:var(--muted)">DIR</td>
1031
+ <td class="actions">
1032
+ <button class="btn btn-mini" onclick="openFolder('${escapeAttr(name)}')">Open</button>
1033
+ <button class="btn btn-mini" onclick="renameDir('${escapeAttr(name)}')">Rename</button>
1034
+ <button class="btn btn-mini btn-danger" onclick="deleteDir('${escapeAttr(name)}')">Delete</button>
1035
+ </td>
1036
+ </tr>
1037
+ `);
1038
+ }
1039
+ for (const f of (data.files || [])) {
1040
+ const relFile = currentPath ? `${currentPath}/${f.name}` : f.name;
1041
+ rows.push(`
1042
+ <tr>
1043
+ <td><span class="name-link" onclick="openEditor('${escapeAttr(relFile)}')">📄 ${escapeHtml(f.name)}</span></td>
1044
+ <td style="font-family:var(--mono); font-size:12px;">${escapeHtml(f.size || '')}</td>
1045
+ <td class="actions">
1046
+ <button class="btn btn-mini" onclick="renameFilePrompt('${escapeAttr(f.name)}')">Rename</button>
1047
+ <button class="btn btn-mini btn-danger" onclick="deleteFilePrompt('${escapeAttr(f.name)}')">Delete</button>
1048
+ </td>
1049
+ </tr>
1050
+ `);
1051
+ }
1052
+ document.getElementById('fileList').innerHTML = rows.join('');
1053
+ }
1054
+
1055
+ function openFolder(name) { currentPath = currentPath ? (currentPath + '/' + name) : name; loadFiles(); }
1056
+ function jumpPath(p) { currentPath = p || ''; loadFiles(); }
1057
+ function goUp() {
1058
+ if (!currentPath) return;
1059
+ const parts = currentPath.split('/').filter(Boolean); parts.pop();
1060
+ currentPath = parts.join('/'); loadFiles();
1061
+ }
1062
+
1063
+ function newFolder() {
1064
+ openM("Initialize Directory", "", async (v) => {
1065
+ await fetch(`/files/mkdir/${encodeURIComponent(currentSrv.key)}`, {
1066
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentPath, name: v })
1067
+ });
1068
+ loadFiles(); closeM();
1069
+ });
1070
+ }
1071
+ function renameDir(oldName) {
1072
+ openM("Move / Rename Directory", oldName, async (v) => {
1073
+ await fetch(`/files/rename/${encodeURIComponent(currentSrv.key)}`, {
1074
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentPath, old: oldName, new: v })
1075
+ });
1076
+ loadFiles(); closeM();
1077
+ });
1078
+ }
1079
+ async function deleteDir(name) {
1080
+ if (!confirm(`Verify deletion protocol on directory '${name}'? This action cannot be reversed.`)) return;
1081
+ await fetch(`/files/delete/${encodeURIComponent(currentSrv.key)}`, {
1082
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentPath, name, kind: 'dir' })
1083
+ });
1084
+ loadFiles();
1085
+ }
1086
+ function renameFilePrompt(oldName) {
1087
+ openM("Move / Rename File", oldName, async (v) => {
1088
+ await fetch(`/files/rename/${encodeURIComponent(currentSrv.key)}`, {
1089
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentPath, old: oldName, new: v })
1090
+ });
1091
+ loadFiles(); closeM();
1092
+ });
1093
+ }
1094
+ async function deleteFilePrompt(name) {
1095
+ if (!confirm(`Verify deletion protocol on file '${name}'?`)) return;
1096
+ await fetch(`/files/delete/${encodeURIComponent(currentSrv.key)}`, {
1097
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: currentPath, name, kind: 'file' })
1098
+ });
1099
+ loadFiles();
1100
+ }
1101
+ async function doUploadFiles(input) {
1102
+ if (!input.files || !input.files.length) return;
1103
+ const fd = new FormData();
1104
+ for (const f of input.files) fd.append("files", f);
1105
+ await fetch(`/files/upload/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`, { method: "POST", body: fd });
1106
+ input.value = ""; loadFiles();
1107
+ }
1108
+ async function doUploadFolder(input) {
1109
+ if (!input.files || !input.files.length) return;
1110
+ const fd = new FormData();
1111
+ for (const f of input.files) { fd.append("files", f); fd.append("relpaths", f.webkitRelativePath || f.name); }
1112
+ await fetch(`/files/upload/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`, { method: "POST", body: fd });
1113
+ input.value = ""; loadFiles();
1114
+ }
1115
+ async function openEditor(name) {
1116
+ currentFile = name; document.getElementById('ed-title').textContent = name;
1117
+ const res = await fetch(`/files/content/${encodeURIComponent(currentSrv.key)}?file=${encodeURIComponent(name)}`);
1118
+ if (res.redirected) { safeRedirect(res.url); return; }
1119
+ const data = await res.json();
1120
+ document.getElementById('ed-area').value = data.content || '';
1121
+ document.getElementById('editor-modal').style.display = 'block';
1122
+ }
1123
+ async function saveFile() {
1124
+ await fetch(`/files/save/${encodeURIComponent(currentSrv.key)}`, {
1125
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1126
+ body: JSON.stringify({ file: currentFile, content: document.getElementById('ed-area').value })
1127
+ });
1128
+ alert("Write success ✅");
1129
+ }
1130
+ function closeEditor() { document.getElementById('editor-modal').style.display = 'none'; }
1131
+
1132
+ function openM(t, v, cb) {
1133
+ document.getElementById('modalBackdrop').style.display = 'flex';
1134
+ document.getElementById('mTitle').textContent = t;
1135
+ document.getElementById('mIn').value = v || "";
1136
+ document.getElementById('mConfirm').onclick = () => cb(document.getElementById('mIn').value);
1137
+ document.getElementById('mIn').focus();
1138
+ }
1139
+ function closeM() { document.getElementById('modalBackdrop').style.display = 'none'; }
1140
+
1141
+ document.getElementById('newBtn').onclick = () => openM("Initialize Node (Alias)", "", async (v) => {
1142
+ const res = await fetch('/add', {
1143
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: v })
1144
+ });
1145
+ const d = await res.json().catch(() => ({ success: false, message: 'Error during initialization' }));
1146
+ if (!d.success) { alert(d.message || 'Initialization Failed'); return; }
1147
+ closeM(); loadServers(); loadPythonHost();
1148
+ });
1149
+
1150
+ function escapeHtml(s) {
1151
+ return (s || '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", "&#039;");
1152
+ }
1153
+ function escapeAttr(s) { return escapeHtml(s).replaceAll("\\", "\\\\"); }
1154
+
1155
+ document.addEventListener('DOMContentLoaded', () => { loadServers(); loadPythonHost(); });
1156
+ </script>
1157
+ </body>
1158
+
1159
+ </html>
login.html ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>الذئب الابيض · @j49_c</title>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
10
+
11
+ :root {
12
+ --bg: #000000;
13
+ --surface: rgba(28, 28, 30, 0.6);
14
+ --surface-border: rgba(255, 255, 255, 0.1);
15
+ --primary: #0A84FF;
16
+ --primary-hover: #0071E3;
17
+ --text: #F5F5F7;
18
+ --muted: #86868B;
19
+ --error: #FF453A;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ min-height: 100vh;
30
+ display: grid;
31
+ place-items: center;
32
+ color: var(--text);
33
+ background-color: var(--bg);
34
+ background-image:
35
+ radial-gradient(circle at 15% 50%, rgba(10, 132, 255, 0.15), transparent 25%),
36
+ radial-gradient(circle at 85% 30%, rgba(255, 255, 255, 0.05), transparent 25%);
37
+ overflow: hidden;
38
+ }
39
+
40
+ body::before {
41
+ content: '';
42
+ position: absolute;
43
+ top: 0;
44
+ left: 0;
45
+ right: 0;
46
+ height: 1px;
47
+ background: linear-gradient(90deg, transparent, rgba(10, 132, 255, 0.5), transparent);
48
+ }
49
+
50
+ .container {
51
+ position: relative;
52
+ z-index: 10;
53
+ animation: fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
54
+ opacity: 0;
55
+ transform: translateY(20px);
56
+ }
57
+
58
+ @keyframes fadeUp {
59
+ to {
60
+ opacity: 1;
61
+ transform: translateY(0);
62
+ }
63
+ }
64
+
65
+ .card {
66
+ width: min(420px, 92vw);
67
+ background: var(--surface);
68
+ border: 1px solid var(--surface-border);
69
+ border-radius: 24px;
70
+ backdrop-filter: blur(40px);
71
+ -webkit-backdrop-filter: blur(40px);
72
+ padding: 40px 32px;
73
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.1);
74
+ text-align: center;
75
+ }
76
+
77
+ .logo {
78
+ font-size: 42px;
79
+ margin-bottom: 16px;
80
+ display: inline-block;
81
+ background: linear-gradient(135deg, #fff, #a1a1a6);
82
+ -webkit-background-clip: text;
83
+ -webkit-text-fill-color: transparent;
84
+ }
85
+
86
+ .title {
87
+ font-size: 24px;
88
+ font-weight: 700;
89
+ letter-spacing: -0.5px;
90
+ margin-bottom: 8px;
91
+ }
92
+
93
+ .sub {
94
+ color: var(--muted);
95
+ font-size: 14px;
96
+ margin-bottom: 32px;
97
+ font-weight: 400;
98
+ line-height: 1.5;
99
+ }
100
+
101
+ .input-group {
102
+ position: relative;
103
+ margin-bottom: 16px;
104
+ text-align: left;
105
+ }
106
+
107
+ input {
108
+ width: 100%;
109
+ padding: 14px 16px;
110
+ border-radius: 12px;
111
+ border: 1px solid rgba(255, 255, 255, 0.12);
112
+ background: rgba(0, 0, 0, 0.3);
113
+ color: var(--text);
114
+ font-size: 15px;
115
+ outline: none;
116
+ transition: all 0.2s ease;
117
+ }
118
+
119
+ input::placeholder {
120
+ color: #6e6e73;
121
+ }
122
+
123
+ input:focus {
124
+ border-color: var(--primary);
125
+ background: rgba(0, 0, 0, 0.5);
126
+ box-shadow: 0 0 0 4px rgba(10, 132, 255, 0.15);
127
+ }
128
+
129
+ .actions {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 12px;
133
+ margin-top: 24px;
134
+ }
135
+
136
+ .btn {
137
+ width: 100%;
138
+ padding: 14px;
139
+ border-radius: 12px;
140
+ font-size: 15px;
141
+ font-weight: 600;
142
+ cursor: pointer;
143
+ transition: all 0.2s;
144
+ border: none;
145
+ outline: none;
146
+ }
147
+
148
+ .btn-primary {
149
+ background: var(--primary);
150
+ color: white;
151
+ box-shadow: 0 4px 14px rgba(10, 132, 255, 0.3);
152
+ }
153
+
154
+ .btn-primary:hover {
155
+ background: var(--primary-hover);
156
+ transform: translateY(-1px);
157
+ }
158
+
159
+ .btn-primary:active {
160
+ transform: translateY(1px);
161
+ }
162
+
163
+ .btn-secondary {
164
+ background: transparent;
165
+ color: var(--primary);
166
+ border: 1px solid transparent;
167
+ }
168
+
169
+ .btn-secondary:hover {
170
+ background: rgba(10, 132, 255, 0.1);
171
+ }
172
+
173
+ .msg {
174
+ margin-top: 16px;
175
+ color: var(--muted);
176
+ font-size: 13px;
177
+ min-height: 20px;
178
+ }
179
+
180
+ .footer {
181
+ margin-top: 32px;
182
+ font-size: 12px;
183
+ color: #424245;
184
+ }
185
+ </style>
186
+ </head>
187
+
188
+ <body>
189
+ <div class="container">
190
+ <div class="card">
191
+ <div class="logo"></div>
192
+ <div class="title">الذئب الابيض</div>
193
+ <div class="sub">Central Intelligence Node<br>Authorized Personnel Only</div>
194
+
195
+ <div class="input-group">
196
+ <input id="u" placeholder="Agent ID / Username" autocomplete="username">
197
+ </div>
198
+ <div class="input-group">
199
+ <input id="p" placeholder="Passcode" type="password" autocomplete="current-password">
200
+ </div>
201
+
202
+ <div class="actions">
203
+ <button class="btn btn-primary" onclick="doLogin()">Authenticate</button>
204
+ </div>
205
+
206
+ <div class="msg" id="m"></div>
207
+ </div>
208
+ <div class="footer">All rights reserved. الذئب الابيض - @j49_c</div>
209
+ </div>
210
+
211
+ <script>
212
+ async function doLogin() {
213
+ const m = document.getElementById('m');
214
+ m.textContent = 'Authenticating...';
215
+ const username = document.getElementById('u').value.trim();
216
+ const password = document.getElementById('p').value;
217
+ const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) });
218
+ const d = await r.json().catch(() => ({ success: false, message: 'Server error' }));
219
+ if (!r.ok || !d.success) { m.style.color = "var(--error)"; m.textContent = d.message || 'Authentication failed'; return; }
220
+ m.style.color = "#32D74B";
221
+ m.textContent = 'Access Granted.';
222
+ setTimeout(() => location.href = d.is_admin ? '/admin' : '/', 500);
223
+ }
224
+ </script>
225
+ </body>
226
+
227
+ </html>
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask>=2.2.0
2
+ Werkzeug>=2.2.0
3
+ psutil>=5.9.0