infinityonline commited on
Commit
835ed07
·
verified ·
1 Parent(s): 7a433fd

Update dashboard.html

Browse files
Files changed (1) hide show
  1. dashboard.html +461 -129
dashboard.html CHANGED
@@ -3,147 +3,437 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Duck.ai API Dashboard</title>
 
7
  <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body { background: #0f1117; color: #e0e0e0; font-family: 'Segoe UI', sans-serif; padding: 20px; }
10
- h1 { text-align: center; color: #4fc3f7; margin-bottom: 24px; font-size: 1.6rem; }
11
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
12
- .card { background: #1e2130; border-radius: 12px; padding: 20px; border: 1px solid #2a2d3e; }
13
- .card .label { font-size: 0.8rem; color: #888; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
14
- .card .value { font-size: 2rem; font-weight: bold; }
15
- .card .value.green { color: #69f0ae; }
16
- .card .value.blue { color: #4fc3f7; }
17
- .card .value.orange { color: #ffb74d; }
18
- .card .value.red { color: #ef5350; }
19
- .card .value.purple { color: #ce93d8; }
20
- .section { background: #1e2130; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid #2a2d3e; }
21
- .section h2 { color: #4fc3f7; margin-bottom: 16px; font-size: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  table { width: 100%; border-collapse: collapse; }
23
- th { text-align: right; padding: 10px; background: #252840; color: #aaa; font-size: 0.8rem; }
24
- td { padding: 10px; border-bottom: 1px solid #2a2d3e; font-size: 0.9rem; }
25
- .badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 0.75rem; }
26
- .badge.busy { background: #ef535020; color: #ef5350; border: 1px solid #ef535050; }
27
- .badge.free { background: #69f0ae20; color: #69f0ae; border: 1px solid #69f0ae50; }
28
- .bar-wrap { background: #252840; border-radius: 20px; height: 10px; overflow: hidden; margin-top: 6px; }
29
- .bar { height: 100%; border-radius: 20px; transition: width 0.5s; background: linear-gradient(90deg, #4fc3f7, #69f0ae); }
30
- .bar.warn { background: linear-gradient(90deg, #ffb74d, #ef5350); }
31
- .log-box { background: #0d1117; border-radius: 8px; padding: 14px; font-family: monospace; font-size: 0.8rem;
32
- height: 220px; overflow-y: auto; color: #ccc; border: 1px solid #2a2d3e; }
33
- .log-box .ok { color: #69f0ae; }
34
- .log-box .err { color: #ef5350; }
35
- .log-box .info { color: #4fc3f7; }
36
- .log-box .warn { color: #ffb74d; }
37
- .refresh-badge { text-align: center; color: #555; font-size: 0.75rem; margin-top: 10px; }
38
- .chart-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
39
- canvas { background: #141720; border-radius: 8px; border: 1px solid #2a2d3e; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  /* ── Login Overlay ── */
42
  #login-overlay {
43
  display: none;
44
  position: fixed; inset: 0;
45
- background: #0f1117ee;
 
46
  z-index: 999;
47
  align-items: center;
48
  justify-content: center;
49
  }
50
  #login-overlay.show { display: flex; }
 
51
  .login-box {
52
- background: #1e2130;
53
- border: 1px solid #2a2d3e;
54
- border-radius: 16px;
55
- padding: 40px;
56
- width: 360px;
57
  text-align: center;
 
 
 
58
  }
59
- .login-box h2 { color: #4fc3f7; margin-bottom: 8px; font-size: 1.3rem; }
60
- .login-box p { color: #888; font-size: 0.85rem; margin-bottom: 24px; }
 
 
 
 
 
 
 
 
 
 
 
 
61
  .login-box input {
62
- width: 100%; padding: 12px 16px;
63
- background: #0d1117; border: 1px solid #2a2d3e;
64
- border-radius: 8px; color: #e0e0e0;
65
- font-size: 0.95rem; margin-bottom: 16px;
 
66
  outline: none; direction: ltr;
 
 
67
  }
68
- .login-box input:focus { border-color: #4fc3f7; }
69
- .login-box button {
70
- width: 100%; padding: 12px;
71
- background: linear-gradient(90deg, #4fc3f7, #69f0ae);
72
- border: none; border-radius: 8px;
73
- color: #0f1117; font-weight: bold;
74
- font-size: 1rem; cursor: pointer;
 
 
75
  }
76
- .login-box button:hover { opacity: 0.9; }
77
- .login-err { color: #ef5350; font-size: 0.82rem; margin-top: 10px; display: none; }
78
-
79
- /* ── Logout btn ── */
80
- #logout-btn {
81
- position: fixed; top: 16px; left: 16px;
82
- background: #1e2130; border: 1px solid #2a2d3e;
83
- color: #888; padding: 6px 14px;
84
- border-radius: 8px; cursor: pointer;
85
- font-size: 0.8rem; z-index: 100;
86
  }
87
- #logout-btn:hover { color: #ef5350; border-color: #ef535050; }
 
 
 
 
 
 
 
 
88
  </style>
89
  </head>
90
  <body>
91
 
92
- <!-- Login Overlay -->
93
  <div id="login-overlay" class="show">
94
  <div class="login-box">
95
- <h2>🦆 Duck.ai Dashboard</h2>
96
- <p>أدخل API Key للدخول</p>
97
- <input type="password" id="key-input" placeholder="API Key..." />
98
- <button onclick="doLogin()">دخول ←</button>
 
99
  <div class="login-err" id="login-err">❌ مفتاح خاطئ، حاول مجدداً</div>
100
  </div>
101
  </div>
102
 
103
- <!-- Logout -->
104
  <button id="logout-btn" onclick="doLogout()">🔓 خروج</button>
105
 
106
- <h1>🦆 Duck.ai API لوحة المراقبة</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- <!-- KPI Cards -->
109
  <div class="grid">
110
- <div class="card">
 
111
  <div class="label">إجمالي الطلبات</div>
112
  <div class="value blue" id="total_req">—</div>
113
  </div>
114
- <div class="card">
 
115
  <div class="label">طلبات اليوم</div>
116
  <div class="value green" id="today_req">—</div>
117
  </div>
118
- <div class="card">
 
119
  <div class="label">مرفوضة (Queue Full)</div>
120
  <div class="value red" id="rejected">—</div>
121
  </div>
122
- <div class="card">
 
123
  <div class="label">Workers حرة</div>
124
  <div class="value green" id="workers_free">—</div>
125
  </div>
126
- <div class="card">
 
127
  <div class="label">Workers مشغولة</div>
128
  <div class="value orange" id="workers_busy">—</div>
129
  </div>
130
- <div class="card">
 
131
  <div class="label">RAM المستخدم</div>
132
  <div class="value purple" id="ram_used">—</div>
133
  </div>
134
- <div class="card">
 
135
  <div class="label">RAM الكلي</div>
136
- <div class="value blue" id="ram_total">—</div>
137
  </div>
138
- <div class="card">
 
139
  <div class="label">CPU %</div>
140
  <div class="value orange" id="cpu">—</div>
141
  </div>
142
  </div>
143
 
144
- <!-- Workers Table -->
145
  <div class="section">
146
- <h2>⚙️ حالة Workers</h2>
 
 
 
147
  <table>
148
  <thead>
149
  <tr>
@@ -158,29 +448,40 @@
158
  </table>
159
  </div>
160
 
161
- <!-- Charts -->
162
- <div class="chart-wrap" style="margin-bottom:20px;">
163
- <div class="section" style="margin:0;">
164
- <h2>📈 الطلبات (آخر 60 دقيقة)</h2>
165
- <canvas id="reqChart" height="120"></canvas>
 
 
 
166
  </div>
167
- <div class="section" style="margin:0;">
168
- <h2>💾 RAM % (آخر 60 دقيقة)</h2>
169
- <canvas id="ramChart" height="120"></canvas>
 
 
 
170
  </div>
171
  </div>
172
 
173
- <!-- Live Log -->
174
  <div class="section">
175
- <h2>📋 السجل المباشر</h2>
 
 
 
176
  <div class="log-box" id="log_box"></div>
177
  </div>
178
 
179
- <div class="refresh-badge">
180
- يتجدد كل <span id="interval_val">5</span> ثوان
181
- آخر تحديث: <span id="last_update"></span>
182
  </div>
183
 
 
 
184
  <script>
185
  // ====================================================================
186
  // Auth
@@ -188,7 +489,6 @@
188
  const urlToken = new URLSearchParams(window.location.search).get('token');
189
  if (urlToken) {
190
  localStorage.setItem('duck_token', urlToken);
191
- // أزل token من الـ URL بعد حفظه
192
  window.history.replaceState({}, '', window.location.pathname);
193
  }
194
 
@@ -200,7 +500,7 @@ function doLogin() {
200
  TOKEN = val;
201
  localStorage.setItem('duck_token', TOKEN);
202
  document.getElementById('login-err').style.display = 'none';
203
- fetchData(); // جرب مباشرة
204
  }
205
 
206
  function doLogout() {
@@ -219,12 +519,10 @@ function showLogin(msg) {
219
  }
220
  }
221
 
222
- // Enter key في input
223
  document.getElementById('key-input').addEventListener('keydown', e => {
224
  if (e.key === 'Enter') doLogin();
225
  });
226
 
227
- // إذا عنده token محفوظ ابدأ مباشرة
228
  if (TOKEN) {
229
  document.getElementById('login-overlay').classList.remove('show');
230
  }
@@ -237,7 +535,6 @@ let reqHistory = Array(60).fill(0);
237
  let ramHistory = Array(60).fill(0);
238
  let logs = [];
239
  let lastTotal = 0;
240
- let fetchTimer = null;
241
 
242
  const todayKey = () => new Date().toISOString().slice(0, 10);
243
  let todayCount = parseInt(localStorage.getItem('today_' + todayKey()) || '0');
@@ -260,18 +557,27 @@ async function fetchData() {
260
  const d = await r.json();
261
  document.getElementById('login-overlay').classList.remove('show');
262
 
263
- // ── KPIs ─────────────────────────────────────────────
264
- document.getElementById('total_req').textContent = d.total_requests ?? '';
 
 
 
 
 
 
 
265
  document.getElementById('rejected').textContent = d.rejected_requests ?? 0;
266
- document.getElementById('workers_free').textContent = d.workers_free ?? '—';
267
- document.getElementById('workers_busy').textContent = d.workers_busy ?? '—';
268
 
269
  if (d.ram) {
270
  document.getElementById('ram_used').textContent = d.ram.used_gb + ' GB';
271
  document.getElementById('ram_total').textContent = d.ram.total_gb + ' GB';
272
  }
273
  if (d.cpu !== undefined) {
274
- document.getElementById('cpu').textContent = d.cpu + '%';
 
 
275
  }
276
 
277
  // ── Today counter ─────────────────────────────────────
@@ -286,87 +592,113 @@ async function fetchData() {
286
  localStorage.setItem('today_' + todayKey(), todayCount);
287
  lastTotal = d.total_requests ?? 0;
288
  }
289
- document.getElementById('today_req').textContent = todayCount;
290
 
291
  // ── Workers table ─────────────────────────────────────
292
  const tbody = document.getElementById('workers_table');
293
  tbody.innerHTML = '';
294
  (d.workers || []).forEach(w => {
295
- const used = (parseInt(d.pool_size) || 30) - w.requests_until_rotation;
296
- const max = parseInt(d.pool_size) || 30;
297
- const pct = Math.round((used / max) * 100);
298
  const warn = pct > 80 ? 'warn' : '';
299
  tbody.innerHTML += `
300
  <tr>
301
- <td>W${w.id}</td>
302
- <td><span class="badge ${w.busy ? 'busy' : 'free'}">${w.busy ? '🔴 مشغول' : '🟢 حر'}</span></td>
303
- <td>${w.total_requests}</td>
304
- <td>${w.requests_until_rotation}</td>
305
- <td style="width:160px">
306
- <div class="bar-wrap">
307
- <div class="bar ${warn}" style="width:${pct}%"></div>
 
 
 
308
  </div>
309
  </td>
310
  </tr>`;
311
  });
312
 
313
- // ── History charts ───────────────��────────────────────
314
  reqHistory.push(diff > 0 ? diff : 0); reqHistory.shift();
315
  if (d.ram) { ramHistory.push(d.ram.percent); ramHistory.shift(); }
316
 
317
- drawChart('reqChart', reqHistory, '#4fc3f7');
318
- drawChart('ramChart', ramHistory, '#ce93d8', 100);
319
 
320
  // ── Log ───────────────────────────────────────────────
321
  addLog(
322
- `total=${d.total_requests} | free=${d.workers_free} | busy=${d.workers_busy} | RAM=${d.ram?.percent ?? '?'}% | CPU=${d.cpu ?? '?'}%`,
323
  'ok'
324
  );
325
  document.getElementById('last_update').textContent =
326
  new Date().toLocaleTimeString('ar-EG');
327
 
328
  } catch (e) {
329
- addLog('فشل الاتصال: ' + e.message, 'err');
330
  }
331
  }
332
 
333
  function addLog(msg, cls = 'info') {
334
  const now = new Date().toLocaleTimeString('ar-EG');
335
- logs.unshift(`<span class="${cls}">[${now}] ${msg}</span>`);
336
  if (logs.length > 80) logs.pop();
337
  document.getElementById('log_box').innerHTML = logs.join('<br>');
338
  }
339
 
340
- function drawChart(id, data, color, maxVal = null) {
341
  const canvas = document.getElementById(id);
342
  const ctx = canvas.getContext('2d');
343
- canvas.width = canvas.offsetWidth;
344
- canvas.height = 120;
 
345
  const W = canvas.width, H = canvas.height;
346
  const max = maxVal ?? Math.max(...data, 1);
347
  ctx.clearRect(0, 0, W, H);
348
 
349
- ctx.strokeStyle = '#2a2d3e'; ctx.lineWidth = 1;
 
 
350
  [0.25, 0.5, 0.75].forEach(f => {
351
  ctx.beginPath(); ctx.moveTo(0, H * f); ctx.lineTo(W, H * f); ctx.stroke();
352
  });
353
 
 
 
 
 
 
 
 
 
 
354
  const step = W / (data.length - 1);
355
  ctx.beginPath();
356
  data.forEach((v, i) => {
357
  const x = i * step;
358
- const y = H - (v / max) * (H - 10) - 5;
359
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
360
  });
361
- ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
 
 
 
 
 
 
 
 
362
  ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath();
363
- ctx.fillStyle = color + '22'; ctx.fill();
 
364
  }
365
 
366
  // ====================================================================
367
  // Start
368
  // ====================================================================
369
- document.getElementById('interval_val').textContent = REFRESH / 1000;
 
 
370
  if (TOKEN) fetchData();
371
  setInterval(fetchData, REFRESH);
372
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🦆 Duck.ai Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
  <style>
9
+ :root {
10
+ --bg: #080b14;
11
+ --surface: #0f1420;
12
+ --surface2: #161c2d;
13
+ --border: #1e2640;
14
+ --border2: #252d45;
15
+ --text: #d4daf0;
16
+ --muted: #5a6280;
17
+ --blue: #4fc3f7;
18
+ --green: #43e97b;
19
+ --orange: #f9a825;
20
+ --red: #ef5350;
21
+ --purple: #b39ddb;
22
+ --cyan: #26c6da;
23
+ --glow-blue: 0 0 20px #4fc3f730;
24
+ --glow-green:0 0 20px #43e97b30;
25
+ --radius: 14px;
26
+ }
27
+
28
+ * { margin:0; padding:0; box-sizing:border-box; }
29
+
30
+ body {
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ font-family: 'Inter', 'Segoe UI', sans-serif;
34
+ padding: 24px;
35
+ min-height: 100vh;
36
+ background-image:
37
+ radial-gradient(ellipse 800px 400px at 20% 0%, #0d1f3c44 0%, transparent 70%),
38
+ radial-gradient(ellipse 600px 300px at 80% 100%, #1a0d2e33 0%, transparent 70%);
39
+ }
40
+
41
+ /* ── Header ── */
42
+ .header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ margin-bottom: 28px;
47
+ position: relative;
48
+ }
49
+ .header h1 {
50
+ font-size: 1.5rem;
51
+ font-weight: 700;
52
+ background: linear-gradient(135deg, #4fc3f7, #43e97b);
53
+ -webkit-background-clip: text;
54
+ -webkit-text-fill-color: transparent;
55
+ background-clip: text;
56
+ letter-spacing: -0.3px;
57
+ }
58
+ .header-sub {
59
+ text-align: center;
60
+ color: var(--muted);
61
+ font-size: 0.78rem;
62
+ margin-top: 4px;
63
+ margin-bottom: 28px;
64
+ }
65
+ .live-dot {
66
+ display: inline-block;
67
+ width: 7px; height: 7px;
68
+ background: var(--green);
69
+ border-radius: 50%;
70
+ margin-left: 6px;
71
+ animation: pulse 2s infinite;
72
+ vertical-align: middle;
73
+ }
74
+ @keyframes pulse {
75
+ 0%,100% { opacity:1; box-shadow: 0 0 0 0 #43e97b60; }
76
+ 50% { opacity:.7; box-shadow: 0 0 0 5px #43e97b00; }
77
+ }
78
+
79
+ /* ── Logout ── */
80
+ #logout-btn {
81
+ position: fixed; top: 20px; left: 20px;
82
+ background: var(--surface2);
83
+ border: 1px solid var(--border);
84
+ color: var(--muted);
85
+ padding: 7px 14px;
86
+ border-radius: 8px;
87
+ cursor: pointer;
88
+ font-size: 0.78rem;
89
+ z-index: 100;
90
+ transition: all .2s;
91
+ font-family: inherit;
92
+ }
93
+ #logout-btn:hover { color: var(--red); border-color: #ef535040; background: #ef535010; }
94
+
95
+ /* ── KPI Grid ── */
96
+ .grid {
97
+ display: grid;
98
+ grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
99
+ gap: 14px;
100
+ margin-bottom: 20px;
101
+ }
102
+
103
+ .card {
104
+ background: var(--surface);
105
+ border-radius: var(--radius);
106
+ padding: 18px 20px;
107
+ border: 1px solid var(--border);
108
+ position: relative;
109
+ overflow: hidden;
110
+ transition: border-color .3s, transform .2s;
111
+ }
112
+ .card:hover { border-color: var(--border2); transform: translateY(-1px); }
113
+ .card::before {
114
+ content: '';
115
+ position: absolute; top:0; left:0; right:0;
116
+ height: 2px;
117
+ background: var(--card-accent, linear-gradient(90deg, #4fc3f7, #43e97b));
118
+ opacity: .7;
119
+ }
120
+ .card.accent-green { --card-accent: linear-gradient(90deg,#43e97b,#26c6da); }
121
+ .card.accent-red { --card-accent: linear-gradient(90deg,#ef5350,#f9a825); }
122
+ .card.accent-orange { --card-accent: linear-gradient(90deg,#f9a825,#ff8f00); }
123
+ .card.accent-purple { --card-accent: linear-gradient(90deg,#b39ddb,#7e57c2); }
124
+ .card.accent-blue { --card-accent: linear-gradient(90deg,#4fc3f7,#0288d1); }
125
+ .card.accent-cyan { --card-accent: linear-gradient(90deg,#26c6da,#43e97b); }
126
+
127
+ .card .icon {
128
+ font-size: 1.4rem;
129
+ margin-bottom: 10px;
130
+ display: block;
131
+ }
132
+ .card .label {
133
+ font-size: 0.7rem;
134
+ color: var(--muted);
135
+ text-transform: uppercase;
136
+ letter-spacing: 1.2px;
137
+ margin-bottom: 6px;
138
+ font-weight: 500;
139
+ }
140
+ .card .value {
141
+ font-size: 1.9rem;
142
+ font-weight: 700;
143
+ line-height: 1;
144
+ }
145
+ .card .value.green { color: var(--green); }
146
+ .card .value.blue { color: var(--blue); }
147
+ .card .value.orange { color: var(--orange); }
148
+ .card .value.red { color: var(--red); }
149
+ .card .value.purple { color: var(--purple); }
150
+ .card .value.cyan { color: var(--cyan); }
151
+
152
+ /* ── Section ── */
153
+ .section {
154
+ background: var(--surface);
155
+ border-radius: var(--radius);
156
+ padding: 20px 22px;
157
+ margin-bottom: 18px;
158
+ border: 1px solid var(--border);
159
+ }
160
+ .section-header {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 8px;
164
+ margin-bottom: 18px;
165
+ }
166
+ .section-header h2 {
167
+ color: var(--text);
168
+ font-size: 0.9rem;
169
+ font-weight: 600;
170
+ letter-spacing: 0.2px;
171
+ }
172
+ .section-header .sh-icon {
173
+ font-size: 1rem;
174
+ }
175
+
176
+ /* ── Table ── */
177
  table { width: 100%; border-collapse: collapse; }
178
+ thead tr { border-bottom: 1px solid var(--border); }
179
+ th {
180
+ text-align: right;
181
+ padding: 10px 12px;
182
+ color: var(--muted);
183
+ font-size: 0.72rem;
184
+ font-weight: 600;
185
+ text-transform: uppercase;
186
+ letter-spacing: .8px;
187
+ }
188
+ td {
189
+ padding: 12px;
190
+ border-bottom: 1px solid var(--border);
191
+ font-size: 0.87rem;
192
+ color: var(--text);
193
+ }
194
+ tbody tr:last-child td { border-bottom: none; }
195
+ tbody tr:hover td { background: #ffffff05; }
196
+
197
+ /* ── Badge ── */
198
+ .badge {
199
+ display: inline-flex;
200
+ align-items: center;
201
+ gap: 5px;
202
+ padding: 4px 12px;
203
+ border-radius: 20px;
204
+ font-size: 0.72rem;
205
+ font-weight: 600;
206
+ }
207
+ .badge.busy { background: #ef535015; color: var(--red); border: 1px solid #ef535035; }
208
+ .badge.free { background: #43e97b15; color: var(--green); border: 1px solid #43e97b35; }
209
+
210
+ /* ── Progress Bar ── */
211
+ .bar-wrap {
212
+ background: var(--surface2);
213
+ border-radius: 20px;
214
+ height: 8px;
215
+ overflow: hidden;
216
+ }
217
+ .bar {
218
+ height: 100%;
219
+ border-radius: 20px;
220
+ transition: width .6s cubic-bezier(.4,0,.2,1);
221
+ background: linear-gradient(90deg, var(--blue), var(--cyan));
222
+ }
223
+ .bar.warn { background: linear-gradient(90deg, var(--orange), var(--red)); }
224
+
225
+ /* ── Charts ── */
226
+ .chart-wrap {
227
+ display: grid;
228
+ grid-template-columns: 1fr 1fr;
229
+ gap: 18px;
230
+ margin-bottom: 18px;
231
+ }
232
+ @media (max-width: 700px) {
233
+ .chart-wrap { grid-template-columns: 1fr; }
234
+ .grid { grid-template-columns: repeat(2, 1fr); }
235
+ }
236
+ canvas {
237
+ background: transparent;
238
+ border-radius: 8px;
239
+ display: block;
240
+ width: 100%;
241
+ }
242
+
243
+ /* ── Log ── */
244
+ .log-box {
245
+ background: #040608;
246
+ border-radius: 8px;
247
+ padding: 14px 16px;
248
+ font-family: 'Courier New', monospace;
249
+ font-size: 0.78rem;
250
+ height: 200px;
251
+ overflow-y: auto;
252
+ color: var(--muted);
253
+ border: 1px solid var(--border);
254
+ line-height: 1.8;
255
+ }
256
+ .log-box::-webkit-scrollbar { width: 4px; }
257
+ .log-box::-webkit-scrollbar-track { background: transparent; }
258
+ .log-box::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
259
+ .log-box .ok { color: var(--green); }
260
+ .log-box .err { color: var(--red); }
261
+ .log-box .info { color: var(--blue); }
262
+ .log-box .warn { color: var(--orange); }
263
+
264
+ /* ── Footer ── */
265
+ .footer {
266
+ text-align: center;
267
+ color: var(--muted);
268
+ font-size: 0.73rem;
269
+ margin-top: 16px;
270
+ padding: 10px 0;
271
+ }
272
 
273
  /* ── Login Overlay ── */
274
  #login-overlay {
275
  display: none;
276
  position: fixed; inset: 0;
277
+ background: #080b14f0;
278
+ backdrop-filter: blur(12px);
279
  z-index: 999;
280
  align-items: center;
281
  justify-content: center;
282
  }
283
  #login-overlay.show { display: flex; }
284
+
285
  .login-box {
286
+ background: var(--surface);
287
+ border: 1px solid var(--border2);
288
+ border-radius: 20px;
289
+ padding: 44px 40px;
290
+ width: 380px;
291
  text-align: center;
292
+ box-shadow: 0 40px 80px #00000080, var(--glow-blue);
293
+ position: relative;
294
+ overflow: hidden;
295
  }
296
+ .login-box::before {
297
+ content: '';
298
+ position: absolute; top:0; left:0; right:0; height:2px;
299
+ background: linear-gradient(90deg, #4fc3f7, #43e97b);
300
+ }
301
+ .login-duck { font-size: 3rem; margin-bottom: 12px; display: block; }
302
+ .login-box h2 {
303
+ font-size: 1.25rem; font-weight: 700;
304
+ background: linear-gradient(135deg, #4fc3f7, #43e97b);
305
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
306
+ background-clip: text;
307
+ margin-bottom: 6px;
308
+ }
309
+ .login-box p { color: var(--muted); font-size: 0.83rem; margin-bottom: 28px; }
310
  .login-box input {
311
+ width: 100%; padding: 13px 16px;
312
+ background: var(--surface2);
313
+ border: 1px solid var(--border);
314
+ border-radius: 10px; color: var(--text);
315
+ font-size: 0.93rem; margin-bottom: 14px;
316
  outline: none; direction: ltr;
317
+ transition: border-color .2s;
318
+ font-family: inherit;
319
  }
320
+ .login-box input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px #4fc3f715; }
321
+ .login-btn {
322
+ width: 100%; padding: 13px;
323
+ background: linear-gradient(135deg, #4fc3f7, #43e97b);
324
+ border: none; border-radius: 10px;
325
+ color: #050810; font-weight: 700;
326
+ font-size: 0.97rem; cursor: pointer;
327
+ transition: opacity .2s, transform .1s;
328
+ font-family: inherit;
329
  }
330
+ .login-btn:hover { opacity: .92; }
331
+ .login-btn:active { transform: scale(.98); }
332
+ .login-err { color: var(--red); font-size: 0.8rem; margin-top: 12px; display: none; }
333
+
334
+ /* ── Uptime badge ── */
335
+ .uptime-row {
336
+ display: flex; gap: 10px; flex-wrap: wrap;
337
+ margin-bottom: 18px;
 
 
338
  }
339
+ .uptime-chip {
340
+ background: var(--surface2);
341
+ border: 1px solid var(--border);
342
+ border-radius: 20px;
343
+ padding: 5px 14px;
344
+ font-size: 0.75rem;
345
+ color: var(--muted);
346
+ }
347
+ .uptime-chip span { color: var(--text); font-weight: 600; }
348
  </style>
349
  </head>
350
  <body>
351
 
352
+ <!-- ═══════════ LOGIN ═══════════ -->
353
  <div id="login-overlay" class="show">
354
  <div class="login-box">
355
+ <span class="login-duck">🦆</span>
356
+ <h2>Duck.ai Dashboard</h2>
357
+ <p>أدخل API Key للدخول إلى لوحة المراقبة</p>
358
+ <input type="password" id="key-input" placeholder="sk-..." autocomplete="current-password" />
359
+ <button class="login-btn" onclick="doLogin()">دخول ←</button>
360
  <div class="login-err" id="login-err">❌ مفتاح خاطئ، حاول مجدداً</div>
361
  </div>
362
  </div>
363
 
364
+ <!-- ═══════════ LOGOUT ═══════════ -->
365
  <button id="logout-btn" onclick="doLogout()">🔓 خروج</button>
366
 
367
+ <!-- ═══════════ HEADER ═══════════ -->
368
+ <div class="header">
369
+ <div>
370
+ <h1>🦆 Duck.ai API &nbsp;<span style="font-size:.9rem;font-weight:400;opacity:.6">لوحة المراقبة</span></h1>
371
+ </div>
372
+ </div>
373
+ <div class="header-sub">
374
+ <span class="live-dot"></span>
375
+ مراقبة مباشرة — يتجدد كل <strong id="interval_val" style="color:var(--text)">5</strong> ثوان
376
+ &nbsp;•&nbsp; آخر تحديث: <strong id="last_update" style="color:var(--text)">—</strong>
377
+ </div>
378
+
379
+ <!-- ═══════════ UPTIME CHIPS ═══════════ -->
380
+ <div class="uptime-row" id="uptime_row" style="display:none">
381
+ <div class="uptime-chip">Pool Size: <span id="chip_pool">—</span></div>
382
+ <div class="uptime-chip">Queue Timeout: <span id="chip_timeout">—</span>s</div>
383
+ <div class="uptime-chip">طلبات مرفوضة: <span id="chip_rej">0</span></div>
384
+ <div class="uptime-chip">RAM %: <span id="chip_ram">—</span></div>
385
+ </div>
386
 
387
+ <!-- ═══════════ KPI GRID ═══════════ -->
388
  <div class="grid">
389
+ <div class="card accent-blue">
390
+ <span class="icon">📊</span>
391
  <div class="label">إجمالي الطلبات</div>
392
  <div class="value blue" id="total_req">—</div>
393
  </div>
394
+ <div class="card accent-green">
395
+ <span class="icon">📅</span>
396
  <div class="label">طلبات اليوم</div>
397
  <div class="value green" id="today_req">—</div>
398
  </div>
399
+ <div class="card accent-red">
400
+ <span class="icon">🚫</span>
401
  <div class="label">مرفوضة (Queue Full)</div>
402
  <div class="value red" id="rejected">—</div>
403
  </div>
404
+ <div class="card accent-green">
405
+ <span class="icon">✅</span>
406
  <div class="label">Workers حرة</div>
407
  <div class="value green" id="workers_free">—</div>
408
  </div>
409
+ <div class="card accent-orange">
410
+ <span class="icon">⚙️</span>
411
  <div class="label">Workers مشغولة</div>
412
  <div class="value orange" id="workers_busy">—</div>
413
  </div>
414
+ <div class="card accent-purple">
415
+ <span class="icon">💾</span>
416
  <div class="label">RAM المستخدم</div>
417
  <div class="value purple" id="ram_used">—</div>
418
  </div>
419
+ <div class="card accent-cyan">
420
+ <span class="icon">🖥️</span>
421
  <div class="label">RAM الكلي</div>
422
+ <div class="value cyan" id="ram_total">—</div>
423
  </div>
424
+ <div class="card accent-orange">
425
+ <span class="icon">🔥</span>
426
  <div class="label">CPU %</div>
427
  <div class="value orange" id="cpu">—</div>
428
  </div>
429
  </div>
430
 
431
+ <!-- ═══════════ WORKERS TABLE ═══════════ -->
432
  <div class="section">
433
+ <div class="section-header">
434
+ <span class="sh-icon">⚙️</span>
435
+ <h2>حالة Workers</h2>
436
+ </div>
437
  <table>
438
  <thead>
439
  <tr>
 
448
  </table>
449
  </div>
450
 
451
+ <!-- ═══════════ CHARTS ═══════════ -->
452
+ <div class="chart-wrap">
453
+ <div class="section" style="margin:0">
454
+ <div class="section-header">
455
+ <span class="sh-icon">📈</span>
456
+ <h2>الطلبات — آخر 60 دقيقة</h2>
457
+ </div>
458
+ <canvas id="reqChart"></canvas>
459
  </div>
460
+ <div class="section" style="margin:0">
461
+ <div class="section-header">
462
+ <span class="sh-icon">💾</span>
463
+ <h2>RAM % — آخر 60 دقيقة</h2>
464
+ </div>
465
+ <canvas id="ramChart"></canvas>
466
  </div>
467
  </div>
468
 
469
+ <!-- ═══════════ LIVE LOG ═══════════ -->
470
  <div class="section">
471
+ <div class="section-header">
472
+ <span class="sh-icon">📋</span>
473
+ <h2>السجل المباشر</h2>
474
+ </div>
475
  <div class="log-box" id="log_box"></div>
476
  </div>
477
 
478
+ <div class="footer">
479
+ 🦆 Duck.ai API Pool Server &nbsp;&nbsp;
480
+ يتجدد كل <span id="interval_val2">5</span> ثوان
481
  </div>
482
 
483
+
484
+ <!-- ═══════════ SCRIPT (منطق محفوظ بالكامل) ═══════════ -->
485
  <script>
486
  // ====================================================================
487
  // Auth
 
489
  const urlToken = new URLSearchParams(window.location.search).get('token');
490
  if (urlToken) {
491
  localStorage.setItem('duck_token', urlToken);
 
492
  window.history.replaceState({}, '', window.location.pathname);
493
  }
494
 
 
500
  TOKEN = val;
501
  localStorage.setItem('duck_token', TOKEN);
502
  document.getElementById('login-err').style.display = 'none';
503
+ fetchData();
504
  }
505
 
506
  function doLogout() {
 
519
  }
520
  }
521
 
 
522
  document.getElementById('key-input').addEventListener('keydown', e => {
523
  if (e.key === 'Enter') doLogin();
524
  });
525
 
 
526
  if (TOKEN) {
527
  document.getElementById('login-overlay').classList.remove('show');
528
  }
 
535
  let ramHistory = Array(60).fill(0);
536
  let logs = [];
537
  let lastTotal = 0;
 
538
 
539
  const todayKey = () => new Date().toISOString().slice(0, 10);
540
  let todayCount = parseInt(localStorage.getItem('today_' + todayKey()) || '0');
 
557
  const d = await r.json();
558
  document.getElementById('login-overlay').classList.remove('show');
559
 
560
+ // ── Uptime chips ──────────────────────────────────────
561
+ document.getElementById('uptime_row').style.display = 'flex';
562
+ document.getElementById('chip_pool').textContent = d.pool_size ?? '—';
563
+ document.getElementById('chip_timeout').textContent = d.queue_timeout_sec ?? '—';
564
+ document.getElementById('chip_rej').textContent = d.rejected_requests ?? 0;
565
+ document.getElementById('chip_ram').textContent = d.ram?.percent ? d.ram.percent + '%' : '—';
566
+
567
+ // ── KPIs ──────────────────────────────────────────────
568
+ document.getElementById('total_req').textContent = (d.total_requests ?? 0).toLocaleString();
569
  document.getElementById('rejected').textContent = d.rejected_requests ?? 0;
570
+ document.getElementById('workers_free').textContent = d.workers_free ?? '—';
571
+ document.getElementById('workers_busy').textContent = d.workers_busy ?? '—';
572
 
573
  if (d.ram) {
574
  document.getElementById('ram_used').textContent = d.ram.used_gb + ' GB';
575
  document.getElementById('ram_total').textContent = d.ram.total_gb + ' GB';
576
  }
577
  if (d.cpu !== undefined) {
578
+ const cpuEl = document.getElementById('cpu');
579
+ cpuEl.textContent = d.cpu + '%';
580
+ cpuEl.className = 'value ' + (d.cpu > 80 ? 'red' : d.cpu > 50 ? 'orange' : 'green');
581
  }
582
 
583
  // ── Today counter ─────────────────────────────────────
 
592
  localStorage.setItem('today_' + todayKey(), todayCount);
593
  lastTotal = d.total_requests ?? 0;
594
  }
595
+ document.getElementById('today_req').textContent = todayCount.toLocaleString();
596
 
597
  // ── Workers table ─────────────────────────────────────
598
  const tbody = document.getElementById('workers_table');
599
  tbody.innerHTML = '';
600
  (d.workers || []).forEach(w => {
601
+ const maxR = parseInt(d.pool_size) || 30;
602
+ const used = maxR - w.requests_until_rotation;
603
+ const pct = Math.round((used / maxR) * 100);
604
  const warn = pct > 80 ? 'warn' : '';
605
  tbody.innerHTML += `
606
  <tr>
607
+ <td><strong style="color:var(--blue)">W${w.id}</strong></td>
608
+ <td><span class="badge ${w.busy ? 'busy' : 'free'}">${w.busy ? ' مشغول' : ' حر'}</span></td>
609
+ <td>${w.total_requests.toLocaleString()}</td>
610
+ <td><span style="color:${w.requests_until_rotation < 5 ? 'var(--red)' : 'var(--text)'}">${w.requests_until_rotation}</span></td>
611
+ <td style="min-width:140px">
612
+ <div style="display:flex;align-items:center;gap:8px">
613
+ <div class="bar-wrap" style="flex:1">
614
+ <div class="bar ${warn}" style="width:${pct}%"></div>
615
+ </div>
616
+ <span style="font-size:.72rem;color:var(--muted);width:30px;text-align:left">${pct}%</span>
617
  </div>
618
  </td>
619
  </tr>`;
620
  });
621
 
622
+ // ── History ───────────────────────────────────────────
623
  reqHistory.push(diff > 0 ? diff : 0); reqHistory.shift();
624
  if (d.ram) { ramHistory.push(d.ram.percent); ramHistory.shift(); }
625
 
626
+ drawChart('reqChart', reqHistory, '#4fc3f7', '#26c6da');
627
+ drawChart('ramChart', ramHistory, '#b39ddb', '#7e57c2', 100);
628
 
629
  // ── Log ───────────────────────────────────────────────
630
  addLog(
631
+ `total=${(d.total_requests??0).toLocaleString()} | free=${d.workers_free} | busy=${d.workers_busy} | RAM=${d.ram?.percent ?? '?'}% | CPU=${d.cpu ?? '?'}%`,
632
  'ok'
633
  );
634
  document.getElementById('last_update').textContent =
635
  new Date().toLocaleTimeString('ar-EG');
636
 
637
  } catch (e) {
638
+ addLog('فشل الاتصال: ' + e.message, 'err');
639
  }
640
  }
641
 
642
  function addLog(msg, cls = 'info') {
643
  const now = new Date().toLocaleTimeString('ar-EG');
644
+ logs.unshift(`<span class="${cls}">[${now}]</span> <span style="color:#3a4060">›</span> ${msg}`);
645
  if (logs.length > 80) logs.pop();
646
  document.getElementById('log_box').innerHTML = logs.join('<br>');
647
  }
648
 
649
+ function drawChart(id, data, color1, color2, maxVal = null) {
650
  const canvas = document.getElementById(id);
651
  const ctx = canvas.getContext('2d');
652
+ const rect = canvas.parentElement.getBoundingClientRect();
653
+ canvas.width = canvas.parentElement.clientWidth - 44;
654
+ canvas.height = 130;
655
  const W = canvas.width, H = canvas.height;
656
  const max = maxVal ?? Math.max(...data, 1);
657
  ctx.clearRect(0, 0, W, H);
658
 
659
+ // grid
660
+ ctx.strokeStyle = '#1e264030';
661
+ ctx.lineWidth = 1;
662
  [0.25, 0.5, 0.75].forEach(f => {
663
  ctx.beginPath(); ctx.moveTo(0, H * f); ctx.lineTo(W, H * f); ctx.stroke();
664
  });
665
 
666
+ // gradient fill
667
+ const grad = ctx.createLinearGradient(0, 0, W, 0);
668
+ grad.addColorStop(0, color1);
669
+ grad.addColorStop(1, color2 || color1);
670
+
671
+ const areaGrad = ctx.createLinearGradient(0, 0, 0, H);
672
+ areaGrad.addColorStop(0, color1 + '44');
673
+ areaGrad.addColorStop(1, color1 + '00');
674
+
675
  const step = W / (data.length - 1);
676
  ctx.beginPath();
677
  data.forEach((v, i) => {
678
  const x = i * step;
679
+ const y = H - (v / max) * (H - 14) - 7;
680
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
681
  });
682
+
683
+ // stroke
684
+ ctx.strokeStyle = grad;
685
+ ctx.lineWidth = 2.5;
686
+ ctx.lineJoin = 'round';
687
+ ctx.lineCap = 'round';
688
+ ctx.stroke();
689
+
690
+ // area
691
  ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath();
692
+ ctx.fillStyle = areaGrad;
693
+ ctx.fill();
694
  }
695
 
696
  // ====================================================================
697
  // Start
698
  // ====================================================================
699
+ document.getElementById('interval_val').textContent = REFRESH / 1000;
700
+ document.getElementById('interval_val2').textContent = REFRESH / 1000;
701
+
702
  if (TOKEN) fetchData();
703
  setInterval(fetchData, REFRESH);
704
  </script>