| #!/usr/bin/env python3 | |
| import subprocess, json, threading, time, glob, os | |
| from http.server import HTTPServer, BaseHTTPRequestHandler | |
| HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Docker & Build Monitor</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { background: #0d1117; color: #e6edf3; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; } | |
| header { background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px; display: flex; align-items: center; gap: 12px; } | |
| header h1 { font-size: 1.2rem; color: #58a6ff; font-weight: 600; } | |
| .badge { background: #238636; color: #fff; font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: bold; } | |
| .grid { display: grid; grid-template-columns: 320px 1fr; height: calc(100vh - 57px); } | |
| .sidebar { background: #161b22; border-right: 1px solid #30363d; overflow-y: auto; padding: 16px; } | |
| .sidebar h2 { font-size: 11px; color: #8b949e; text-transform: uppercase; margin-bottom: 10px; margin-top: 15px; letter-spacing: 0.5px; } | |
| .sidebar h2:first-of-type { margin-top: 0; } | |
| .container-card { background: #21262d; border: 1px solid #30363d; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; cursor: pointer; transition: all .2s; } | |
| .container-card:hover, .container-card.active { border-color: #58a6ff; background: #30363d; } | |
| .container-card .name { font-size: 13px; font-weight: 600; color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .container-card .status { font-size: 11px; margin-top: 4px; display: flex; align-items: center; gap: 6px; } | |
| .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } | |
| .dot.up { background: #3fb950; box-shadow: 0 0 6px #3fb950; } | |
| .dot.down { background: #f85149; } | |
| .dot.other { background: #d29922; } | |
| .stats { font-size: 11px; color: #8b949e; margin-top: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .main { display: flex; flex-direction: column; overflow: hidden; } | |
| .tabs { background: #161b22; border-bottom: 1px solid #30363d; display: flex; } | |
| .tab { padding: 12px 24px; font-size: 13px; cursor: pointer; color: #8b949e; border-bottom: 2px solid transparent; transition: all .2s; font-weight: 500; } | |
| .tab.active { color: #e6edf3; border-bottom-color: #58a6ff; font-weight: 600; } | |
| .tab:hover { color: #e6edf3; } | |
| .panel { flex: 1; overflow: hidden; display: none; flex-direction: column; } | |
| .panel.active { display: flex; } | |
| .log-box { flex: 1; background: #0d1117; padding: 16px; overflow-y: auto; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; font-size: 12px; line-height: 1.6; } | |
| .log-line { border-bottom: 1px solid #161b22; padding: 2px 0; word-break: break-all; white-space: pre-wrap; } | |
| .log-line:hover { background: #161b22; } | |
| .log-controls { background: #161b22; border-top: 1px solid #30363d; padding: 12px 16px; display: flex; gap: 16px; align-items: center; } | |
| .log-controls label { font-size: 12px; color: #8b949e; display: flex; align-items: center; gap: 6px; cursor: pointer; } | |
| .log-controls input[type=checkbox] { accent-color: #58a6ff; } | |
| .btn { background: #21262d; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: 500; } | |
| .btn:hover { background: #30363d; border-color: #8b949e; } | |
| .stats-grid { padding: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; overflow-y: auto; } | |
| .stat-card { background: #21262d; border: 1px solid #30363d; border-radius: 8px; padding: 20px; } | |
| .stat-card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; margin-bottom: 8px; font-weight: 600; letter-spacing: 0.5px; } | |
| .stat-card .value { font-size: 24px; font-weight: 700; color: #58a6ff; } | |
| .stat-card .sub { font-size: 11px; color: #8b949e; margin-top: 6px; } | |
| .no-select { color: #8b949e; text-align: center; padding: 40px; font-size: 14px; } | |
| .toolbar { background: #161b22; border-bottom: 1px solid #30363d; padding: 8px 20px; display: flex; align-items: center; gap: 12px; } | |
| .toolbar span { font-size: 12px; color: #8b949e; } | |
| .toolbar strong { color: #58a6ff; font-size: 14px; font-weight: 600; } | |
| .warn { color: #d29922; } .err { color: #f85149; } .info { color: #58a6ff; } .ok { color: #3fb950; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2.5"/><rect x="2" y="14" width="20" height="8" rx="2.5"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> | |
| <h1>Docker & Build Monitor</h1> | |
| <span class="badge" id="live-badge">● LIVE</span> | |
| </header> | |
| <div class="grid"> | |
| <div class="sidebar"> | |
| <h2>Active CLI Tasks / Builds</h2> | |
| <div id="task-list">Loading...</div> | |
| <h2>Docker Containers</h2> | |
| <div id="container-list">Loading...</div> | |
| </div> | |
| <div class="main"> | |
| <div id="no-select" class="no-select" style="display:flex;align-items:center;justify-content:center;height:100%"> | |
| <div>← Select a build task or container from the sidebar</div> | |
| </div> | |
| <div id="container-detail" style="display:none;flex-direction:column;height:100%"> | |
| <div class="toolbar"> | |
| <strong id="detail-name">—</strong> | |
| <span id="detail-status">—</span> | |
| <span style="flex:1"></span> | |
| <span id="detail-image" style="font-size:11px;color:#8b949e"></span> | |
| </div> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('logs')">📜 Logs</div> | |
| <div class="tab" onclick="switchTab('stats')">📊 Stats & Info</div> | |
| </div> | |
| <div class="panel active" id="panel-logs"> | |
| <div class="log-box" id="log-box"> | |
| <div style="color:#8b949e">Select an item...</div> | |
| </div> | |
| <div class="log-controls"> | |
| <label><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label> | |
| <label><input type="checkbox" id="auto-refresh" checked> Live refresh (2s)</label> | |
| <button class="btn" onclick="clearLogs()">Clear</button> | |
| <span id="log-count" style="font-size:11px;color:#8b949e;margin-left:auto"></span> | |
| </div> | |
| </div> | |
| <div class="panel" id="panel-stats"> | |
| <div class="stats-grid" id="stats-grid"> | |
| <div style="color:#8b949e;padding:20px">Loading...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let selectedId = null; | |
| let refreshTimer = null; | |
| let statsTimer = null; | |
| let logLines = []; | |
| function colorize(line) { | |
| if (/ERROR|FATAL|Exception|failed|FAILED|refused/.test(line)) return `<span class="err">${escHtml(line)}</span>`; | |
| if (/WARN|WARNING/.test(line)) return `<span class="warn">${escHtml(line)}</span>`; | |
| if (/INFO|Started|Running|SUCCESS|BUILD SUCCESS|SUCCESSFUL/.test(line)) return `<span class="ok">${escHtml(line)}</span>`; | |
| if (/Downloading|Downloaded|Sending build context/.test(line)) return `<span class="info">${escHtml(line)}</span>`; | |
| return escHtml(line); | |
| } | |
| function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } | |
| async function loadSidebar() { | |
| try { | |
| const r = await fetch('/api/containers'); | |
| const data = await r.json(); | |
| const tasks = data.filter(c => c.is_task); | |
| const containers = data.filter(c => !c.is_task); | |
| // Render Tasks | |
| const taskList = document.getElementById('task-list'); | |
| if (tasks.length === 0) { | |
| taskList.innerHTML = '<div style="font-size:12px;color:#8b949e;padding:8px 4px;">No recent tasks found</div>'; | |
| } else { | |
| taskList.innerHTML = tasks.map(t => { | |
| const active = t.id === selectedId ? 'active' : ''; | |
| const isRunning = t.status === 'Running'; | |
| return `<div class="container-card ${active}" onclick="selectItem('${escHtml(t.id)}','${escHtml(t.name)}')"> | |
| <div class="name" style="color:#58a6ff">${escHtml(t.name)}</div> | |
| <div class="status"><span class="dot ${isRunning?'up':'other'}"></span><span style="color:${isRunning?'#3fb950':'#8b949e'}">${escHtml(t.status)}</span></div> | |
| <div class="stats">${escHtml(t.image)}</div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| // Render Containers | |
| const containerList = document.getElementById('container-list'); | |
| if (containers.length === 0) { | |
| containerList.innerHTML = '<div style="font-size:12px;color:#8b949e;padding:8px 4px;">No containers found</div>'; | |
| } else { | |
| containerList.innerHTML = containers.map(c => { | |
| const running = c.status.startsWith('Up'); | |
| const dotClass = running ? 'up' : c.status.startsWith('Exited') ? 'down' : 'other'; | |
| const active = c.id === selectedId ? 'active' : ''; | |
| return `<div class="container-card ${active}" onclick="selectItem('${escHtml(c.id)}','${escHtml(c.name)}')"> | |
| <div class="name">${escHtml(c.name)}</div> | |
| <div class="status"><span class="dot ${dotClass}"></span><span style="color:${running?'#3fb950':dotClass==='down'?'#f85149':'#d29922'}">${escHtml(c.status)}</span></div> | |
| <div class="stats">${escHtml(c.image.substring(0,40))}</div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| } catch(e) {} | |
| } | |
| async function selectItem(id, name) { | |
| selectedId = id; | |
| document.getElementById('no-select').style.display = 'none'; | |
| document.getElementById('container-detail').style.display = 'flex'; | |
| document.getElementById('detail-name').textContent = name; | |
| logLines = []; | |
| document.getElementById('log-box').innerHTML = '<div style="color:#8b949e">Loading logs...</div>'; | |
| loadSidebar(); | |
| loadLogs(); | |
| loadStats(); | |
| clearInterval(refreshTimer); | |
| clearInterval(statsTimer); | |
| refreshTimer = setInterval(() => { if(document.getElementById('auto-refresh').checked) loadLogs(); }, 2000); | |
| statsTimer = setInterval(loadStats, 3000); | |
| } | |
| async function loadLogs() { | |
| if (!selectedId) return; | |
| try { | |
| const r = await fetch('/api/logs?id=' + encodeURIComponent(selectedId) + '&tail=300'); | |
| const text = await r.text(); | |
| const lines = text.split('\\n'); | |
| logLines = lines.filter(l => l.trim() || l === ''); | |
| renderLogs(); | |
| } catch(e) {} | |
| } | |
| function renderLogs() { | |
| const box = document.getElementById('log-box'); | |
| box.innerHTML = logLines.map(l => `<div class="log-line">${colorize(l)}</div>`).join(''); | |
| document.getElementById('log-count').textContent = logLines.length + ' lines'; | |
| if (document.getElementById('auto-scroll').checked) box.scrollTop = box.scrollHeight; | |
| } | |
| function clearLogs() { logLines = []; renderLogs(); } | |
| async function loadStats() { | |
| if (!selectedId) return; | |
| try { | |
| const r = await fetch('/api/stats?id=' + encodeURIComponent(selectedId)); | |
| const data = await r.json(); | |
| document.getElementById('detail-status').textContent = data.status || ''; | |
| document.getElementById('detail-image').textContent = (data.image||'').substring(0,60); | |
| if (selectedId.startsWith('/')) { | |
| // CLI Task Stats View | |
| document.getElementById('stats-grid').innerHTML = ` | |
| <div class="stat-card"><div class="label">Task Status</div><div class="value">${data.status||'—'}</div><div class="sub">running / finished</div></div> | |
| <div class="stat-card"><div class="label">Log Size</div><div class="value">${data.mem_usage||'—'}</div><div class="sub">log size on disk</div></div> | |
| <div class="stat-card"><div class="label">Log File Path</div><div style="font-size:11px;word-break:break-all;color:#58a6ff;margin-top:8px">${data.image||'—'}</div></div> | |
| `; | |
| } else { | |
| // Docker Container Stats View | |
| document.getElementById('stats-grid').innerHTML = ` | |
| <div class="stat-card"><div class="label">CPU Usage</div><div class="value">${data.cpu||'—'}</div><div class="sub">of host CPU</div></div> | |
| <div class="stat-card"><div class="label">Memory</div><div class="value">${data.mem_usage||'—'}</div><div class="sub">limit: ${data.mem_limit||'—'}</div></div> | |
| <div class="stat-card"><div class="label">Memory %</div><div class="value">${data.mem_pct||'—'}</div><div class="sub"> </div></div> | |
| <div class="stat-card"><div class="label">Network IN</div><div class="value">${data.net_in||'—'}</div><div class="sub">received</div></div> | |
| <div class="stat-card"><div class="label">Network OUT</div><div class="value">${data.net_out||'—'}</div><div class="sub">sent</div></div> | |
| <div class="stat-card"><div class="label">Block I/O Read</div><div class="value">${data.block_in||'—'}</div><div class="sub"> </div></div> | |
| <div class="stat-card"><div class="label">Block I/O Write</div><div class="value">${data.block_out||'—'}</div><div class="sub"> </div></div> | |
| <div class="stat-card"><div class="label">PIDs</div><div class="value">${data.pids||'—'}</div><div class="sub">processes</div></div> | |
| `; | |
| } | |
| } catch(e) {} | |
| } | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', ['logs','stats'][i]===tab)); | |
| document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); | |
| document.getElementById('panel-'+tab).classList.add('active'); | |
| } | |
| loadSidebar(); | |
| setInterval(loadSidebar, 5000); | |
| </script> | |
| </body> | |
| </html>""" | |
| def get_recent_tasks(): | |
| files = glob.glob('/Users/tahamajs/.gemini/antigravity-cli/brain/*/.system_generated/tasks/task-*.log') | |
| files.sort(key=os.path.getmtime, reverse=True) | |
| tasks = [] | |
| for f in files[:8]: | |
| mtime = os.path.getmtime(f) | |
| size = os.path.getsize(f) | |
| desc = "Background Process" | |
| try: | |
| with open(f, 'r', errors='replace') as fh: | |
| first_lines = [fh.readline() for _ in range(15)] | |
| for line in first_lines: | |
| line_strip = line.strip() | |
| if line_strip: | |
| if not any(x in line_strip for x in ['██', 'IE Research', '══', '✔', 'ℹ', '⚠']): | |
| if "Task Description:" in line_strip: | |
| desc = line_strip.replace("Task Description:", "").strip() | |
| break | |
| elif "Running command" in line_strip or "CommandLine" in line_strip: | |
| desc = line_strip | |
| break | |
| else: | |
| desc = line_strip[:80] | |
| break | |
| except Exception: | |
| pass | |
| is_active = (time.time() - mtime) < 15 | |
| status = "Running" if is_active else "Finished" | |
| desc = desc.replace('cd /Users/tahamajs/Documents/uni/IE/Projects && ', '') | |
| desc = desc.strip('"\'') | |
| tasks.append({ | |
| "id": f, | |
| "name": desc[:50] + ("..." if len(desc) > 50 else ""), | |
| "status": status, | |
| "image": f"Size: {size/1024:.1f} KB | Path: {os.path.basename(f)}", | |
| "is_task": True | |
| }) | |
| return tasks | |
| class Handler(BaseHTTPRequestHandler): | |
| def log_message(self, *a): pass | |
| def do_GET(self): | |
| from urllib.parse import urlparse, parse_qs | |
| p = urlparse(self.path) | |
| qs = parse_qs(p.query) | |
| if p.path == '/': | |
| self.send_response(200) | |
| self.send_header('Content-Type', 'text/html; charset=utf-8') | |
| self.end_headers() | |
| self.wfile.write(HTML.encode()) | |
| elif p.path == '/api/containers': | |
| items = get_recent_tasks() | |
| try: | |
| out = subprocess.check_output( | |
| ['docker', 'ps', '-a', '--format', | |
| '{"id":"{{.ID}}","name":"{{.Names}}","status":"{{.Status}}","image":"{{.Image}}"}'], | |
| stderr=subprocess.DEVNULL).decode() | |
| containers = [json.loads(l) for l in out.strip().split('\n') if l.strip()] | |
| for c in containers: | |
| c['is_task'] = False | |
| items.extend(containers) | |
| except Exception: | |
| pass | |
| self.send_response(200) | |
| self.send_header('Content-Type', 'application/json') | |
| self.send_header('Access-Control-Allow-Origin', '*') | |
| self.end_headers() | |
| self.wfile.write(json.dumps(items).encode()) | |
| elif p.path == '/api/logs': | |
| cid = qs.get('id', [''])[0] | |
| tail = qs.get('tail', ['300'])[0] | |
| if cid.startswith('/'): | |
| try: | |
| with open(cid, 'r', errors='replace') as fh: | |
| lines = fh.readlines() | |
| try: | |
| t = int(tail) | |
| lines = lines[-t:] | |
| except ValueError: | |
| lines = lines[-300:] | |
| out = "".join(lines) | |
| except Exception as e: | |
| out = f"Error reading log file: {str(e)}" | |
| else: | |
| try: | |
| out = subprocess.check_output( | |
| ['docker', 'logs', '--tail', tail, cid], | |
| stderr=subprocess.STDOUT).decode(errors='replace') | |
| except Exception: | |
| try: | |
| driver = subprocess.check_output( | |
| ['docker', 'inspect', cid, '--format', '{{.HostConfig.LogConfig.Type}}'], | |
| stderr=subprocess.DEVNULL).decode().strip() | |
| if driver == 'none': | |
| out = ("(This container's log driver is set to 'none' by Docker, usually " | |
| "during image build steps. Please look at the corresponding " | |
| "running task log in the 'Active CLI Tasks / Builds' section above!)") | |
| else: | |
| out = f"(could not read logs — driver is {driver})" | |
| except Exception: | |
| out = "(could not read logs — container may not support log driver)" | |
| self.send_response(200) | |
| self.send_header('Content-Type', 'text/plain; charset=utf-8') | |
| self.send_header('Access-Control-Allow-Origin', '*') | |
| self.end_headers() | |
| self.wfile.write(out.encode()) | |
| elif p.path == '/api/stats': | |
| cid = qs.get('id', [''])[0] | |
| if cid.startswith('/'): | |
| try: | |
| mtime = os.path.getmtime(cid) | |
| size = os.path.getsize(cid) | |
| is_active = (time.time() - mtime) < 15 | |
| status = "Running" if is_active else "Finished" | |
| data = { | |
| 'status': status, | |
| 'mem_usage': f"{size/1024:.1f} KB", | |
| 'image': cid, | |
| } | |
| except Exception as e: | |
| data = {'error': str(e)} | |
| else: | |
| try: | |
| raw = subprocess.check_output( | |
| ['docker', 'stats', '--no-stream', '--format', | |
| '{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}', cid], | |
| stderr=subprocess.DEVNULL).decode().strip() | |
| parts = raw.split('|') | |
| mem = parts[1].split(' / ') if len(parts) > 1 else ['?', '?'] | |
| net = parts[3].split(' / ') if len(parts) > 3 else ['?', '?'] | |
| blk = parts[4].split(' / ') if len(parts) > 4 else ['?', '?'] | |
| info = subprocess.check_output( | |
| ['docker', 'inspect', cid, '--format', '{{.State.Status}}|{{.Config.Image}}'], | |
| stderr=subprocess.DEVNULL).decode().strip().split('|') | |
| data = { | |
| 'cpu': parts[0] if parts else '?', | |
| 'mem_usage': mem[0].strip() if mem else '?', | |
| 'mem_limit': mem[1].strip() if len(mem) > 1 else '?', | |
| 'mem_pct': parts[2] if len(parts) > 2 else '?', | |
| 'net_in': net[0].strip() if net else '?', | |
| 'net_out': net[1].strip() if len(net) > 1 else '?', | |
| 'block_in': blk[0].strip() if blk else '?', | |
| 'block_out': blk[1].strip() if len(blk) > 1 else '?', | |
| 'pids': parts[5] if len(parts) > 5 else '?', | |
| 'status': info[0] if info else '?', | |
| 'image': info[1] if len(info) > 1 else '?', | |
| } | |
| except Exception as e: | |
| data = {'error': str(e)} | |
| self.send_response(200) | |
| self.send_header('Content-Type', 'application/json') | |
| self.send_header('Access-Control-Allow-Origin', '*') | |
| self.end_headers() | |
| self.wfile.write(json.dumps(data).encode()) | |
| else: | |
| self.send_response(404) | |
| self.end_headers() | |
| if __name__ == '__main__': | |
| server = HTTPServer(('0.0.0.0', 8888), Handler) | |
| print("🐳 Docker & Build Monitor running at http://localhost:8888") | |
| server.serve_forever() | |
Xet Storage Details
- Size:
- 21.6 kB
- Xet hash:
- 1c3726b7369c989554de1ec31d8309fa42c30fb3932bd6ad7023599b0b7140a7
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.