diff --git a/dashboard/noc.py b/dashboard/noc.py index 6cc8c3b..d3c8f42 100644 --- a/dashboard/noc.py +++ b/dashboard/noc.py @@ -1,434 +1,213 @@ """ -Network Operations Center โ Real-time monitoring dashboard for all 8 publisher sites. -Hacker aesthetic, live data, comprehensive fleet overview. +NOC Dashboard โ clean, modern fleet monitor for all 8 publisher sites. """ -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 +import json, time, threading, requests +from datetime import datetime +from flask import Flask, jsonify 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"}, + "ai": {"name": "AI Insights", "ip": "10.30.20.240", "ct": 135, "color": "#7c3aed"}, + "tech": {"name": "Tech Frontier", "ip": "10.30.20.241", "ct": 136, "color": "#2563eb"}, + "science": {"name": "Science Decoded", "ip": "10.30.20.242", "ct": 137, "color": "#059669"}, + "crypto": {"name": "Crypto Compass", "ip": "10.30.20.243", "ct": 138, "color": "#f59e0b"}, + "linux": {"name": "Linux Lab", "ip": "10.30.20.244", "ct": 139, "color": "#f97316"}, + "gaming": {"name": "Game Layer", "ip": "10.30.20.246", "ct": 140, "color": "#ec4899"}, + "diy": {"name": "Maker Forge", "ip": "10.30.20.247", "ct": 141, "color": "#ef4444"}, + "guides": {"name": "Practical Guides", "ip": "10.30.20.248", "ct": 142, "color": "#0891b2"}, } -# Cached fleet state โ refreshed async -fleet_cache = {"data": {}, "updated": None, "lock": threading.Lock()} +cache = {"data": {}, "ts": None} - -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 +def probe(vertical, info): + s = dict(vertical=vertical, name=info["name"], ip=info["ip"], ct=info["ct"], + color=info["color"], alive=False, articles=0, views=0, today=0, + subs=0, latency=0, arts=[], error=None) try: t0 = time.time() - r = requests.get(f"http://{ip}:80/health", timeout=5) - state["latency_ms"] = round((time.time() - t0) * 1000) - + r = requests.get(f"http://{info['ip']}:80/health", timeout=5) + s["latency"] = 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) + s["alive"] = True + s["articles"] = r.json().get("articles", 0) + r2 = requests.get(f"http://{info['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) + d = r2.json() + s["views"] = d.get("total_views", 0) + s["today"] = d.get("today_views", 0) + r3 = requests.get(f"http://{info['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 - + s["arts"] = [{"t": a.get("title", "")[:65], "s": a.get("slug", ""), + "d": (a.get("published_at") or "")[:10], "w": a.get("word_count", 0)} + for a in r3.json()[:3]] except Exception as e: - state["error"] = str(e)[:80] - - return state + s["error"] = str(e)[:50] + return vertical, s - -def refresh_fleet(): - """Background refresh of all 8 sites.""" +def refresh(): 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() - + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex: + results = dict(ex.map(lambda x: probe(*x), [(v, i) for v, i in SITES.items()])) + cache["data"] = results + cache["ts"] = 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'
' - - if not recent_html: - recent_html = '