diff --git a/dashboard/noc.py b/dashboard/noc.py new file mode 100644 index 0000000..6cc8c3b --- /dev/null +++ b/dashboard/noc.py @@ -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'
{art["date"]}{art["title"][:65]}{art["words"]}w
' + + if not recent_html: + recent_html = '
No articles yet
' + + sites_html += f""" +
+ +
+
+
{s.get('articles', 0)}
+
Articles
+
+
+
{s.get('views', 0)}
+
Views
+
+
+
{s.get('today_views', 0)}
+
Today
+
+
+
{s.get('subscribers', 0)}
+
Subs
+
+
+
{latency}
+
Latency
+
+
+
{(s.get('error') or 'โœ…')[:20]}
+
Status
+
+
+
+ + {recent_html} +
+
""" + + 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 = """ + + + + + NOC โ€” Publisher Fleet Monitor + + + + +
+ +
+ ๐ŸŸข __ALIVE__/__TOTAL__ online + ๐Ÿ• __UPDATED__ + ๐Ÿ”„ auto-refresh 60s +
+
+ +
+
__TOTAL_ARTICLES__
Total Articles
+
__TOTAL_VIEWS__
Total Views
+
__TOTAL_TODAY__
Today Views
+
__TOTAL_SUBS__
Subscribers
+
__ALIVE__/8
Sites Live
+
6AM
Next Publish
+
+ +
+ __SITES_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)