| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width,initial-scale=1"> |
| <title>سجل الأحداث — Sayed Bot</title> |
| <style> |
| *{box-sizing:border-box;margin:0;padding:0} |
| :root{--bg:#0d1117;--surface:rgba(22,27,34,.72);--surface2:rgba(30,39,50,.75);--border:rgba(255,255,255,.06);--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;--green:#3fb950;--green2:#56d364;--green-bg:rgba(63,185,80,.1);--blue:#58a6ff;--blue2:#79c0ff;--blue-bg:rgba(88,166,255,.1);--red:#f85149;--red2:#ff7b72;--red-bg:rgba(248,81,73,.1);--amber:#d29922;--amber-bg:rgba(210,153,34,.1);--radius:12px;--radius-sm:8px;--shadow:0 8px 32px rgba(0,0,0,.35);--font:system-ui,'Segoe UI',Roboto,sans-serif;--glass:rgba(22,27,34,.65);--glass-blur:20px} |
| html,body{min-height:100vh} |
| body{background:var(--bg);background-image:radial-gradient(ellipse 80% 50% at 20% 0%,rgba(63,185,80,.06) 0%,transparent 50%),radial-gradient(ellipse 60% 40% at 80% 100%,rgba(88,166,255,.04) 0%,transparent 50%);color:var(--text);font-family:var(--font);font-size:14px;direction:rtl;line-height:1.6} |
| ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{transparent}::-webkit-scrollbar-thumb{background:rgba(63,185,80,.25);border-radius:10px} |
| ::selection{background:rgba(63,185,80,.3);color:#fff} |
| .layout{display:flex;min-height:100vh} |
| .sidebar{width:220px;flex-shrink:0;position:fixed;right:0;top:0;bottom:0;background:var(--glass);backdrop-filter:blur(var(--glass-blur));border-left:1px solid rgba(255,255,255,.08);display:flex;flex-direction:column;z-index:100} |
| @media(max-width:900px){.sidebar{display:none}} |
| .sidebar-header{padding:20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px} |
| .sidebar-logo{width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,var(--green),var(--blue));display:flex;align-items:center;justify-content:center;font-size:20px} |
| .sidebar-brand{flex:1}.sidebar-title{font-size:14px;font-weight:800;color:var(--text)}.sidebar-ver{font-size:10px;color:var(--green);font-weight:700} |
| .sidebar-nav{flex:1;padding:12px} |
| .nav-section{font-size:10px;color:var(--text3);font-weight:700;text-transform:uppercase;letter-spacing:1px;padding:10px 12px 4px} |
| .nav-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);font-size:13px;font-weight:600;margin-bottom:2px;border:1px solid transparent;text-decoration:none;transition:all .2s} |
| .nav-item:hover{background:var(--green-bg);color:var(--green);border-color:rgba(63,185,80,.1)} |
| .nav-item.active{background:var(--green-bg);color:var(--green);border-color:rgba(63,185,80,.2)} |
| .nav-icon{font-size:15px;width:20px;text-align:center} |
| .main{flex:1;margin-right:220px;padding:20px 24px;min-width:0;max-width:1200px} |
| @media(max-width:900px){.main{margin-right:0;padding:16px}} |
| .main-header{margin-bottom:20px} |
| .main-title{font-size:20px;font-weight:900;display:flex;align-items:center;gap:8px} |
| .main-sub{color:var(--text2);font-size:13px;margin-top:4px} |
| .card{background:var(--surface);backdrop-filter:blur(var(--glass-blur));border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-bottom:16px} |
| .card-title{font-size:15px;font-weight:800;margin-bottom:16px;display:flex;align-items:center;gap:8px;padding-bottom:12px;border-bottom:1px solid var(--border)} |
| .log-container{background:rgba(0,0,0,.4);border-radius:var(--radius-sm);padding:12px;font-family:'Cascadia Code','Fira Code','Consolas',monospace;font-size:12px;line-height:1.7;max-height:600px;overflow-y:auto;direction:ltr;text-align:left} |
| .log-empty{color:var(--text3);text-align:center;padding:40px} |
| .log-entry{padding:2px 0;border-bottom:1px solid rgba(255,255,255,.03);white-space:pre-wrap;word-break:break-all} |
| .log-time{color:var(--text3);margin-right:6px} |
| .log-level{display:inline-block;width:48px;text-align:center;border-radius:3px;font-size:10px;font-weight:700;padding:1px 4px;margin:0 4px} |
| .log-level.LOG{color:var(--text2)} |
| .log-level.ERROR{color:var(--red2);background:var(--red-bg)} |
| .log-level.WARN{color:var(--amber);background:var(--amber-bg)} |
| .log-msg{color:var(--text)} |
| .controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap} |
| .btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:var(--radius-sm);font-size:12px;font-weight:700;cursor:pointer;border:1px solid rgba(255,255,255,.08);background:var(--glass);backdrop-filter:blur(10px);color:var(--text2);transition:all .25s;border:none} |
| .btn:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(0,0,0,.25)} |
| .btn-green{background:linear-gradient(135deg,var(--green),#2ea043);color:#fff} |
| .btn-green:hover{box-shadow:0 4px 20px rgba(63,185,80,.35)} |
| .btn-secondary{background:var(--surface2);color:var(--text2);border:1px solid var(--border)} |
| .btn-secondary:hover{background:var(--surface3);color:var(--text)} |
| .auto-scroll{display:flex;align-items:center;gap:6px;color:var(--text2);font-size:12px;cursor:pointer} |
| .auto-scroll input{cursor:pointer} |
| </style> |
| </head> |
| <body> |
|
|
| <div class="sidebar"> |
| <div class="sidebar-header"> |
| <div class="sidebar-logo">🤖</div> |
| <div class="sidebar-brand"> |
| <div class="sidebar-title">Sayed Bot</div> |
| <div class="sidebar-ver">v26 AI</div> |
| </div> |
| </div> |
| <nav class="sidebar-nav"> |
| <div class="nav-section">لوحة التحكم</div> |
| <a class="nav-item" href="/dashboard"><span class="nav-icon">📊</span> الرئيسية</a> |
| <a class="nav-item" href="/setup"><span class="nav-icon">⚙️</span> إعداد السيرفر</a> |
| <a class="nav-item active" href="/logs"><span class="nav-icon">📋</span> السجل</a> |
| <a class="nav-item" href="/features"><span class="nav-icon">🔌</span> الميزات</a> |
| </nav> |
| </div> |
|
|
| <div class="main"> |
| <div class="main-header"> |
| <div> |
| <div class="main-title">📋 سجل الأحداث</div> |
| <div class="main-sub">جميع أحداث البوتات في الوقت الفعلي — يتم تحديثها تلقائياً</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">📝 السجل المباشر</div> |
| <div class="log-container" id="logContainer"> |
| <div class="log-empty" id="logEmpty">⏳ جاري تحميل السجل...</div> |
| </div> |
| <div class="controls"> |
| <button class="btn btn-secondary" onclick="clearLogs()">🗑️ مسح</button> |
| <button class="btn btn-secondary" onclick="loadLogs()">🔄 تحديث</button> |
| <label class="auto-scroll"><input type="checkbox" id="autoScroll" checked> تمرير تلقائي</label> |
| <span style="flex:1"></span> |
| <span id="logCount" style="color:var(--text3);font-size:12px">0</span> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| let logBuffer = []; |
| let streamActive = false; |
| |
| function escapeHtml(str) { |
| const d = document.createElement('div'); |
| d.textContent = str; |
| return d.innerHTML; |
| } |
| |
| function renderLogs(logs) { |
| const container = document.getElementById('logContainer'); |
| const empty = document.getElementById('logEmpty'); |
| if (empty) empty.remove(); |
| |
| for (const entry of logs) { |
| const time = entry.time.slice(11, 19); |
| const level = (entry.level || 'log').toUpperCase(); |
| const msg = entry.message || ''; |
| |
| const div = document.createElement('div'); |
| div.className = 'log-entry'; |
| div.innerHTML = `<span class="log-time">[${time}]</span><span class="log-level ${level}">${level}</span><span class="log-msg">${escapeHtml(msg)}</span>`; |
| container.appendChild(div); |
| } |
| |
| updateCount(); |
| |
| if (document.getElementById('autoScroll').checked) { |
| container.scrollTop = container.scrollHeight; |
| } |
| } |
| |
| function updateCount() { |
| document.getElementById('logCount').textContent = document.getElementById('logContainer').children.length + ' سطر'; |
| } |
| |
| async function loadLogs() { |
| try { |
| const r = await fetch('/api/logs?count=200'); |
| const data = await r.json(); |
| if (data.logs) { |
| const container = document.getElementById('logContainer'); |
| container.innerHTML = ''; |
| logBuffer = data.logs; |
| renderLogs(data.logs); |
| } |
| } catch(_) {} |
| } |
| |
| function connectStream() { |
| if (streamActive) return; |
| const evtSource = new EventSource('/api/logs/stream'); |
| streamActive = true; |
| |
| evtSource.onmessage = function(e) { |
| try { |
| const entry = JSON.parse(e.data); |
| if (entry.type === 'connected') return; |
| const container = document.getElementById('logContainer'); |
| const empty = document.getElementById('logEmpty'); |
| if (empty) empty.remove(); |
| |
| const time = entry.time.slice(11, 19); |
| const level = (entry.level || 'log').toUpperCase(); |
| const msg = entry.message || ''; |
| |
| const div = document.createElement('div'); |
| div.className = 'log-entry'; |
| div.innerHTML = `<span class="log-time">[${time}]</span><span class="log-level ${level}">${level}</span><span class="log-msg">${escapeHtml(msg)}</span>`; |
| container.appendChild(div); |
| |
| |
| while (container.children.length > 500) container.removeChild(container.firstChild); |
| |
| updateCount(); |
| |
| if (document.getElementById('autoScroll').checked) { |
| container.scrollTop = container.scrollHeight; |
| } |
| } catch(_) {} |
| }; |
| |
| evtSource.onerror = function() { |
| streamActive = false; |
| evtSource.close(); |
| setTimeout(connectStream, 3000); |
| }; |
| } |
| |
| function clearLogs() { |
| const container = document.getElementById('logContainer'); |
| container.innerHTML = ''; |
| updateCount(); |
| } |
| |
| loadLogs(); |
| connectStream(); |
| </script> |
|
|
| </body> |
| </html> |
|
|