Spaces:
Paused
Paused
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Video Swarm — السجلات</title> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; --bg-card: rgba(15,15,25,0.95); | |
| --primary: #00ffaa; --secondary: #00aaff; | |
| --text: #e0e0e0; --text-dim: #888; | |
| --border: rgba(0,255,170,0.15); | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| background: var(--bg); color: var(--text); | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| min-height: 100vh; | |
| } | |
| header { | |
| position: sticky; top: 0; z-index: 100; | |
| background: rgba(10,10,15,0.95); backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); padding: 12px 24px; | |
| display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .brand { display: flex; align-items: center; gap: 12px; } | |
| .brand h1 { | |
| font-size: 1.2rem; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .controls { display: flex; gap: 8px; } | |
| .btn { | |
| padding: 8px 16px; border: 1px solid var(--border); | |
| background: rgba(255,255,255,0.03); color: var(--text); | |
| border-radius: 8px; cursor: pointer; transition: all 0.3s; | |
| font-size: 0.85rem; font-family: inherit; | |
| } | |
| .btn:hover { border-color: var(--primary); color: var(--primary); } | |
| .btn.active { background: var(--primary); color: #0a0a0f; border-color: var(--primary); } | |
| .filters { | |
| padding: 16px 24px; display: flex; gap: 12px; align-items: center; | |
| border-bottom: 1px solid var(--border); flex-wrap: wrap; | |
| } | |
| .filters input { | |
| padding: 8px 14px; background: rgba(255,255,255,0.03); | |
| border: 1px solid var(--border); border-radius: 8px; | |
| color: var(--text); outline: none; font-family: inherit; min-width: 200px; | |
| } | |
| .filters input:focus { border-color: var(--primary); } | |
| .level-filter { display: flex; gap: 4px; } | |
| .level-filter .btn { padding: 6px 12px; font-size: 0.8rem; } | |
| .log-container { | |
| padding: 0; max-height: calc(100vh - 140px); | |
| overflow-y: auto; font-family: 'Consolas', 'Monaco', monospace; | |
| } | |
| .log-row { | |
| padding: 8px 24px; border-bottom: 1px solid rgba(255,255,255,0.03); | |
| display: flex; gap: 16px; align-items: flex-start; | |
| font-size: 0.85rem; transition: background 0.2s; | |
| } | |
| .log-row:hover { background: rgba(255,255,255,0.02); } | |
| .log-time { color: #666; white-space: nowrap; min-width: 180px; } | |
| .log-level { | |
| padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; | |
| font-weight: bold; min-width: 60px; text-align: center; | |
| } | |
| .log-agent { color: var(--secondary); min-width: 140px; } | |
| .log-msg { color: var(--text); flex: 1; word-break: break-word; } | |
| /* Level colors */ | |
| .level-INFO { background: rgba(0,255,170,0.1); color: var(--primary); } | |
| .level-WARNING { background: rgba(255,200,0,0.1); color: #ffcc00; } | |
| .level-ERROR { background: rgba(255,0,0,0.15); color: #ff4444; } | |
| .level-TASK { background: rgba(0,170,255,0.1); color: var(--secondary); } | |
| .level-AGENT { background: rgba(170,0,255,0.1); color: #aa55ff; } | |
| .level-DEBUG { background: rgba(255,255,255,0.05); color: #aaa; } | |
| .stats-bar { | |
| padding: 8px 24px; background: rgba(255,255,255,0.02); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; gap: 24px; font-size: 0.8rem; color: var(--text-dim); | |
| } | |
| .stats-bar span { display: flex; align-items: center; gap: 6px; } | |
| ::-webkit-scrollbar { width: 8px; } | |
| ::-webkit-scrollbar-track { background: var(--bg); } | |
| ::-webkit-scrollbar-thumb { background: rgba(0,255,170,0.2); border-radius: 4px; } | |
| .conn-status { | |
| display: flex; align-items: center; gap: 6px; | |
| font-size: 0.8rem; color: var(--text-dim); | |
| } | |
| .conn-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--primary); box-shadow: 0 0 8px var(--primary); | |
| } | |
| .conn-dot.off { background: #ff4444; box-shadow: 0 0 8px #ff4444; } | |
| </style> | |
| <base target="_blank"> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <div style="font-size:1.5rem;">📋</div> | |
| <h1>سجلات النظام — AI Video Swarm</h1> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:16px;"> | |
| <div class="conn-status"> | |
| <div class="conn-dot" id="connDot"></div> | |
| <span id="connText">متصل</span> | |
| </div> | |
| <a href="/app" class="btn">🏠 الرئيسية</a> | |
| <button class="btn" onclick="logout()">خروج</button> | |
| </div> | |
| </header> | |
| <div class="stats-bar" id="statsBar"> | |
| <span>📊 الإجمالي: <strong id="totalCount">0</strong></span> | |
| <span>🟢 INFO: <strong id="infoCount">0</strong></span> | |
| <span>🟡 WARNING: <strong id="warnCount">0</strong></span> | |
| <span>🔴 ERROR: <strong id="errCount">0</strong></span> | |
| <span>🔵 TASK: <strong id="taskCount">0</strong></span> | |
| </div> | |
| <div class="filters"> | |
| <input type="text" id="searchInput" placeholder="🔍 بحث في السجلات..." oninput="applyFilters()"> | |
| <div class="level-filter"> | |
| <button class="btn active" onclick="toggleLevel(this, 'all')">الكل</button> | |
| <button class="btn" onclick="toggleLevel(this, 'INFO')">INFO</button> | |
| <button class="btn" onclick="toggleLevel(this, 'WARNING')">WARNING</button> | |
| <button class="btn" onclick="toggleLevel(this, 'ERROR')">ERROR</button> | |
| <button class="btn" onclick="toggleLevel(this, 'TASK')">TASK</button> | |
| <button class="btn" onclick="toggleLevel(this, 'AGENT')">AGENT</button> | |
| <button class="btn" onclick="toggleLevel(this, 'DEBUG')">DEBUG</button> | |
| </div> | |
| <div style="flex:1;"></div> | |
| <button class="btn" onclick="copyAll()">📋 نسخ الكل</button> | |
| <button class="btn" onclick="clearLogs()">🗑️ مسح</button> | |
| <button class="btn" id="pauseBtn" onclick="togglePause()">⏸️ إيقاف</button> | |
| </div> | |
| <div class="log-container" id="logContainer"> | |
| <div style="padding:40px;text-align:center;color:var(--text-dim);"> | |
| <div style="font-size:2rem;margin-bottom:8px;">📡</div> | |
| <div>جاري الاتصال بخادم السجلات...</div> | |
| </div> | |
| </div> | |
| <script> | |
| let ws = null; | |
| let logs = []; | |
| let paused = false; | |
| let activeLevel = 'all'; | |
| let autoScroll = true; | |
| const LEVEL_EMOJI = { | |
| 'INFO': '🟢', 'WARNING': '🟡', 'ERROR': '🔴', | |
| 'TASK': '🔵', 'AGENT': '🟣', 'DEBUG': '⚪' | |
| }; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| connectWS(); | |
| loadInitialLogs(); | |
| document.getElementById('logContainer').addEventListener('scroll', e => { | |
| const el = e.target; | |
| autoScroll = el.scrollTop + el.clientHeight >= el.scrollHeight - 50; | |
| }); | |
| }); | |
| function connectWS() { | |
| const token = getToken(); | |
| if (!token) return window.location.href = '/'; | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(`${protocol}//${window.location.host}/ws/logs`); | |
| ws.onopen = () => { | |
| document.getElementById('connDot').classList.remove('off'); | |
| document.getElementById('connText').textContent = 'متصل'; | |
| }; | |
| ws.onmessage = e => { | |
| if (paused) return; | |
| try { | |
| const entry = JSON.parse(e.data); | |
| addLog(entry); | |
| } catch(err) {} | |
| }; | |
| ws.onclose = () => { | |
| document.getElementById('connDot').classList.add('off'); | |
| document.getElementById('connText').textContent = 'غير متصل'; | |
| setTimeout(connectWS, 3000); | |
| }; | |
| } | |
| async function loadInitialLogs() { | |
| try { | |
| const res = await fetch('/api/logs?limit=200', { headers: { 'Authorization': 'Bearer ' + getToken() } }); | |
| const data = await res.json(); | |
| logs = data.logs || []; | |
| renderLogs(); | |
| } catch(e) {} | |
| } | |
| function addLog(entry) { | |
| logs.push(entry); | |
| if (logs.length > 10000) logs = logs.slice(-10000); | |
| if (matchesFilter(entry)) { | |
| appendLogRow(entry); | |
| updateStats(); | |
| if (autoScroll) scrollToBottom(); | |
| } | |
| } | |
| function matchesFilter(entry) { | |
| if (activeLevel !== 'all' && entry.level !== activeLevel) return false; | |
| const q = document.getElementById('searchInput').value.toLowerCase(); | |
| if (!q) return true; | |
| return (entry.message && entry.message.toLowerCase().includes(q)) || | |
| (entry.agent && entry.agent.toLowerCase().includes(q)); | |
| } | |
| function renderLogs() { | |
| const container = document.getElementById('logContainer'); | |
| container.innerHTML = ''; | |
| const filtered = logs.filter(matchesFilter); | |
| filtered.forEach(entry => appendLogRow(entry, false)); | |
| updateStats(); | |
| if (autoScroll) scrollToBottom(); | |
| } | |
| function appendLogRow(entry, animate=true) { | |
| const container = document.getElementById('logContainer'); | |
| const row = document.createElement('div'); | |
| row.className = 'log-row'; | |
| if (animate) row.style.animation = 'fadeIn 0.2s ease'; | |
| const ts = entry.timestamp ? new Date(entry.timestamp).toLocaleString('ar-SA') : '--'; | |
| const emoji = LEVEL_EMOJI[entry.level] || '⚪'; | |
| row.innerHTML = ` | |
| <div class="log-time">${ts}</div> | |
| <div class="log-level level-${entry.level}">${emoji} ${entry.level}</div> | |
| <div class="log-agent">[${entry.agent || 'UNKNOWN'}]</div> | |
| <div class="log-msg">${escapeHtml(entry.message || '')}</div> | |
| `; | |
| container.appendChild(row); | |
| } | |
| function updateStats() { | |
| const counts = {}; | |
| logs.forEach(l => { counts[l.level] = (counts[l.level]||0)+1; }); | |
| document.getElementById('totalCount').textContent = logs.length; | |
| document.getElementById('infoCount').textContent = counts['INFO'] || 0; | |
| document.getElementById('warnCount').textContent = counts['WARNING'] || 0; | |
| document.getElementById('errCount').textContent = counts['ERROR'] || 0; | |
| document.getElementById('taskCount').textContent = counts['TASK'] || 0; | |
| } | |
| function scrollToBottom() { | |
| const container = document.getElementById('logContainer'); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function applyFilters() { | |
| renderLogs(); | |
| } | |
| function toggleLevel(btn, level) { | |
| document.querySelectorAll('.level-filter .btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| activeLevel = level; | |
| renderLogs(); | |
| } | |
| function togglePause() { | |
| paused = !paused; | |
| document.getElementById('pauseBtn').textContent = paused ? '▶️ استئناف' : '⏸️ إيقاف'; | |
| } | |
| function clearLogs() { | |
| logs = []; | |
| document.getElementById('logContainer').innerHTML = ''; | |
| updateStats(); | |
| fetch('/api/logs/clear', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() } }).catch(()=>{}); | |
| } | |
| function copyAll() { | |
| const text = logs.map(l => { | |
| const ts = l.timestamp ? new Date(l.timestamp).toISOString() : '--'; | |
| return `[${ts}] [${l.level}] [${l.agent}] — ${l.message}`; | |
| }).join('\n'); | |
| navigator.clipboard.writeText(text).then(() => alert('تم النسخ!')); | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function getToken() { | |
| const match = document.cookie.match(/access_token=([^;]+)/); | |
| return match ? match[1] : ''; | |
| } | |
| function logout() { | |
| document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; | |
| window.location.href = '/'; | |
| } | |
| // Check auth | |
| (async function() { | |
| const token = getToken(); | |
| if (!token) return window.location.href = '/'; | |
| try { | |
| const res = await fetch('/api/auth/verify', { headers: { 'Authorization': 'Bearer ' + token } }); | |
| if (!res.ok) window.location.href = '/'; | |
| } catch(e) { window.location.href = '/'; } | |
| })(); | |
| </script> | |
| <style> | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } | |
| </style> | |
| </body> | |
| </html> |