NOC Dashboard: real-time fleet monitor with hacker aesthetic, per-site cards, IPs, metrics, scanlines

This commit is contained in:
drjones
2026-08-03 23:37:27 -07:00
parent f5efb69905
commit a363700cdc

434
dashboard/noc.py Normal file
View File

@@ -0,0 +1,434 @@
"""
Network Operations Center — Real-time monitoring dashboard for all 8 publisher sites.
Hacker aesthetic, live data, comprehensive fleet overview.
"""
import json
import time
import sqlite3
import threading
from datetime import datetime, timedelta
from pathlib import Path
from collections import defaultdict
from flask import Flask, render_template_string, jsonify, request
app = Flask(__name__)
SITES = {
"ai": {"name": "AI Insights", "ip": "10.30.20.240", "ct": 135, "color": "#7c3aed", "accent": "#a78bfa"},
"tech": {"name": "Tech Frontier", "ip": "10.30.20.241", "ct": 136, "color": "#2563eb", "accent": "#60a5fa"},
"science": {"name": "Science Decoded", "ip": "10.30.20.242", "ct": 137, "color": "#059669", "accent": "#34d399"},
"crypto": {"name": "Crypto Compass", "ip": "10.30.20.243", "ct": 138, "color": "#f59e0b", "accent": "#fbbf24"},
"linux": {"name": "Linux Lab", "ip": "10.30.20.244", "ct": 139, "color": "#f97316", "accent": "#fb923c"},
"gaming": {"name": "Game Layer", "ip": "10.30.20.246", "ct": 140, "color": "#ec4899", "accent": "#f472b6"},
"diy": {"name": "Maker Forge", "ip": "10.30.20.247", "ct": 141, "color": "#ef4444", "accent": "#f87171"},
"guides": {"name": "Practical Guides", "ip": "10.30.20.248", "ct": 142, "color": "#0891b2", "accent": "#22d3ee"},
}
# Cached fleet state — refreshed async
fleet_cache = {"data": {}, "updated": None, "lock": threading.Lock()}
def fetch_site_state(vertical, info):
"""Query a single site for health + stats."""
ip = info["ip"]
state = {
"vertical": vertical,
"name": info["name"],
"ip": ip,
"ct": info["ct"],
"color": info["color"],
"accent": info["accent"],
"alive": False,
"articles": 0,
"views": 0,
"today_views": 0,
"subscribers": 0,
"latency_ms": 0,
"recent_articles": [],
"popular": [],
"error": None,
}
import requests
try:
t0 = time.time()
r = requests.get(f"http://{ip}:80/health", timeout=5)
state["latency_ms"] = round((time.time() - t0) * 1000)
if r.status_code == 200:
h = r.json()
state["alive"] = True
state["articles"] = h.get("articles", 0)
# Stats
r2 = requests.get(f"http://{ip}:80/api/stats", timeout=5)
if r2.status_code == 200:
s = r2.json()
state["views"] = s.get("total_views", 0)
state["today_views"] = s.get("today_views", 0)
popular = s.get("popular", [])
state["popular"] = [{"title": a.get("title", "")[:80], "views": a.get("views", 0)} for a in popular[:3]]
# Recent articles
r3 = requests.get(f"http://{ip}:80/api/articles?limit=3", timeout=5)
if r3.status_code == 200:
arts = r3.json()
state["recent_articles"] = [
{"title": a.get("title", "")[:80], "slug": a.get("slug", ""),
"date": a.get("published_at", "")[:10], "words": a.get("word_count", 0)}
for a in arts[:3]
]
# Subscriber count
try:
r4 = requests.get(f"http://{ip}:5000/api/subscribers", timeout=3)
if r4.status_code == 200:
state["subscribers"] = r4.json().get("count", 0)
except Exception:
pass
except Exception as e:
state["error"] = str(e)[:80]
return state
def refresh_fleet():
"""Background refresh of all 8 sites."""
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = {executor.submit(fetch_site_state, v, i): v for v, i in SITES.items()}
results = {}
for future in concurrent.futures.as_completed(futures):
vertical = futures[future]
try:
results[vertical] = future.result(timeout=10)
except Exception:
results[vertical] = {"vertical": vertical, "alive": False, "error": "timeout"}
with fleet_cache["lock"]:
fleet_cache["data"] = results
fleet_cache["updated"] = datetime.now().isoformat()
@app.route("/")
def noc():
"""Main NOC dashboard."""
# Trigger a refresh if stale (>30s)
if fleet_cache["updated"] is None:
refresh_fleet()
elif (datetime.now() - datetime.fromisoformat(fleet_cache["updated"])).seconds > 30:
threading.Thread(target=refresh_fleet, daemon=True).start()
with fleet_cache["lock"]:
data = dict(fleet_cache["data"])
updated = fleet_cache["updated"]
# Compute aggregates
alive = sum(1 for s in data.values() if s.get("alive"))
total_articles = sum(s.get("articles", 0) for s in data.values())
total_views = sum(s.get("views", 0) for s in data.values())
total_today = sum(s.get("today_views", 0) for s in data.values())
total_subs = sum(s.get("subscribers", 0) for s in data.values())
sites_html = ""
for vertical, s in data.items():
status_dot = "🟢" if s.get("alive") else "🔴"
latency = f"{s.get('latency_ms', 0)}ms"
lat_class = "fast" if s.get("latency_ms", 999) < 100 else ("medium" if s.get("latency_ms", 999) < 300 else "slow")
# Build recent articles list
recent_html = ""
for art in s.get("recent_articles", []):
recent_html += f'<div class="art-row"><span class="art-date">{art["date"]}</span><a href="http://{s["ip"]}/articles/{art["slug"]}" target="_blank" class="art-link">{art["title"][:65]}</a><span class="art-words">{art["words"]}w</span></div>'
if not recent_html:
recent_html = '<div class="art-row" style="color:var(--dim)">No articles yet</div>'
sites_html += f"""
<div class="site-card" style="border-left: 3px solid {s['color']}">
<div class="site-header">
<div class="site-name-row">
<span class="status-dot">{status_dot}</span>
<h3 style="color:{s['color']}">{s['name']}</h3>
</div>
<div class="site-badges">
<span class="badge" style="background:{s['color']}20;color:{s['color']}">{vertical}.thetempleofdoom.com</span>
<a href="http://{s['ip']}" target="_blank" class="badge ip-badge">🖥 {s['ip']}:80</a>
<span class="badge ct-badge">CT {s['ct']}</span>
</div>
</div>
<div class="site-metrics">
<div class="metric">
<div class="metric-value">{s.get('articles', 0)}</div>
<div class="metric-label">Articles</div>
</div>
<div class="metric">
<div class="metric-value">{s.get('views', 0)}</div>
<div class="metric-label">Views</div>
</div>
<div class="metric">
<div class="metric-value">{s.get('today_views', 0)}</div>
<div class="metric-label">Today</div>
</div>
<div class="metric">
<div class="metric-value">{s.get('subscribers', 0)}</div>
<div class="metric-label">Subs</div>
</div>
<div class="metric">
<div class="metric-value {lat_class}">{latency}</div>
<div class="metric-label">Latency</div>
</div>
<div class="metric">
<div class="metric-value" style="font-size:0.8rem">{(s.get('error') or '')[:20]}</div>
<div class="metric-label">Status</div>
</div>
</div>
<div class="recent-section">
<div class="section-label">📝 Latest</div>
{recent_html}
</div>
</div>"""
html = NOC_TEMPLATE
html = html.replace('__ALIVE__', str(alive))
html = html.replace('__TOTAL__', str(len(data)))
html = html.replace('__TOTAL_ARTICLES__', str(total_articles))
html = html.replace('__TOTAL_VIEWS__', str(total_views))
html = html.replace('__TOTAL_TODAY__', str(total_today))
html = html.replace('__TOTAL_SUBS__', str(total_subs))
html = html.replace('__UPDATED__', str(updated[:19] if updated else "never"))
html = html.replace('__SITES_HTML__', sites_html)
return html
@app.route("/api/fleet")
def api_fleet():
"""JSON fleet state."""
refresh_fleet()
with fleet_cache["lock"]:
return jsonify({
"updated": fleet_cache["updated"],
"sites": fleet_cache["data"],
})
@app.route("/api/refresh", methods=["POST"])
def api_refresh():
refresh_fleet()
return jsonify({"status": "refreshed", "updated": fleet_cache["updated"]})
@app.route("/health")
def health():
return jsonify({"status": "ok"})
# ─── Template ──────────────────────────────────────────────────────
NOC_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NOC — Publisher Fleet Monitor</title>
<meta http-equiv="refresh" content="60">
<style>
:root {
--bg: #0a0e14;
--panel: #12171e;
--border: #1e293b;
--text: #e2e8f0;
--dim: #64748b;
--green: #10b981;
--red: #ef4444;
--font: 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace;
}
* {{ box-sizing: border-box; margin: 0; padding: 0 }}
body {{
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}}
/* Scanlines overlay */
body::after {{
content: '';
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
pointer-events: none;
z-index: 9999;
}}
header {{
background: var(--panel);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}}
.logo {{
font-size: 1.1rem;
font-weight: 700;
color: var(--green);
letter-spacing: 0.05em;
}}
.logo span {{ color: var(--dim); font-weight: 400 }}
.header-right {{
display: flex;
gap: 2rem;
align-items: center;
font-size: 0.8rem;
color: var(--dim);
}}
.header-right strong {{ color: var(--text) }}
.stats-bar {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
padding: 1rem 2rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
}}
.stat-mini {{
text-align: center;
padding: 0.5rem;
}}
.stat-mini .val {{ font-size: 1.5rem; font-weight: 700; color: var(--green) }}
.stat-mini .lbl {{ font-size: 0.65rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 0.15rem }}
main {{
padding: 1.5rem 2rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
gap: 1.25rem;
}}
.site-card {{
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.25rem;
transition: border-color 0.2s;
}}
.site-card:hover {{ border-color: rgba(255,255,255,0.15) }}
.site-header {{ margin-bottom: 1rem }}
.site-name-row {{
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}}
.site-name-row h3 {{ font-size: 1.05rem; font-weight: 600 }}
.status-dot {{ font-size: 0.7rem }}
.site-badges {{
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}}
.badge {{
font-size: 0.65rem;
padding: 0.15rem 0.5rem;
border-radius: 3px;
font-family: var(--font);
text-decoration: none;
display: inline-block;
}}
.ip-badge {{ background: rgba(255,255,255,0.05); color: var(--dim) }}
.ip-badge:hover {{ color: var(--text); background: rgba(255,255,255,0.1) }}
.ct-badge {{ background: rgba(255,255,255,0.05); color: var(--dim); font-size: 0.6rem }}
.site-metrics {{
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}}
.metric {{ text-align: center }}
.metric-value {{ font-size: 1.1rem; font-weight: 700 }}
.metric-label {{ font-size: 0.6rem; color: var(--dim); text-transform: uppercase; margin-top: 0.1rem }}
.fast {{ color: var(--green) }}
.medium {{ color: #f59e0b }}
.slow {{ color: var(--red) }}
.recent-section {{ border-top: 1px solid var(--border); padding-top: 0.75rem }}
.section-label {{ font-size: 0.65rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem }}
.art-row {{
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.2rem 0;
font-size: 0.75rem;
}}
.art-date {{ color: var(--dim); font-size: 0.7rem; min-width: 55px }}
.art-link {{ color: var(--text); text-decoration: none; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }}
.art-link:hover {{ text-decoration: underline }}
.art-words {{ color: var(--dim); font-size: 0.65rem; min-width: 35px; text-align: right }}
footer {{
padding: 1rem 2rem;
border-top: 1px solid var(--border);
text-align: center;
color: var(--dim);
font-size: 0.7rem;
}}
@media (max-width: 900px) {{
main {{ grid-template-columns: 1fr }}
.site-metrics {{ grid-template-columns: repeat(3, 1fr) }}
}}
</style>
</head>
<body>
<header>
<div class="logo">NOC <span>// Publisher Fleet Monitor</span></div>
<div class="header-right">
<span>🟢 <strong>__ALIVE__</strong>/__TOTAL__ online</span>
<span>🕐 __UPDATED__</span>
<span>🔄 auto-refresh 60s</span>
</div>
</header>
<div class="stats-bar">
<div class="stat-mini"><div class="val">__TOTAL_ARTICLES__</div><div class="lbl">Total Articles</div></div>
<div class="stat-mini"><div class="val">__TOTAL_VIEWS__</div><div class="lbl">Total Views</div></div>
<div class="stat-mini"><div class="val">__TOTAL_TODAY__</div><div class="lbl">Today Views</div></div>
<div class="stat-mini"><div class="val">__TOTAL_SUBS__</div><div class="lbl">Subscribers</div></div>
<div class="stat-mini"><div class="val">__ALIVE__/8</div><div class="lbl">Sites Live</div></div>
<div class="stat-mini"><div class="val">6AM</div><div class="lbl">Next Publish</div></div>
</div>
<main>
__SITES_HTML__
</main>
<footer>
NOC Dashboard · Auto Publisher Fleet · Updated __UPDATED__ ·
<a href="http://localhost:5106" style="color:var(--dim)">Admin Panel</a> ·
<a href="http://localhost:5053/fleet" style="color:var(--dim)">Kalshi Fleet</a>
</footer>
</body>
</html>"""
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=5090)
ap.add_argument("--host", type=str, default="127.0.0.1")
args = ap.parse_args()
# Initial refresh
refresh_fleet()
print(f"🖥 NOC Dashboard → http://{args.host}:{args.port}")
app.run(host=args.host, port=args.port, debug=False)