""" NOC Dashboard โ clean, modern fleet monitor for all 8 publisher sites. """ import json, time, threading, requests, sys, os from datetime import datetime from flask import Flask, jsonify # Evolution log sys.path.insert(0, os.path.expanduser("~/auto-publisher/core")) from evolution_log import get_recent_evolutions, get_evolution_stats, log_evolution app = Flask(__name__) SITES = { "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"}, } cache = {"data": {}, "ts": None} 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://{info['ip']}:80/health", timeout=5) s["latency"] = round((time.time() - t0) * 1000) if r.status_code == 200: 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: 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: 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: s["error"] = str(e)[:50] return vertical, s def refresh(): import concurrent.futures 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 home(): if not cache["ts"] or (datetime.now() - datetime.fromisoformat(cache["ts"])).seconds > 30: threading.Thread(target=refresh, daemon=True).start() data = cache["data"] or {} evo_stats = get_evolution_stats() evo_recent = get_recent_evolutions(6) evo_html = build_evo_html(evo_recent) return HTML.format( alive=sum(1 for s in data.values() if s.get("alive")), total=len(data), articles=sum(s.get("articles", 0) for s in data.values()), views=sum(s.get("views", 0) for s in data.values()), today=sum(s.get("today", 0) for s in data.values()), subs=sum(s.get("subs", 0) for s in data.values()), updated=(cache["ts"] or "")[:19], evo_total=evo_stats["total"], evo_html=evo_html, cards=build_cards(data), ) def build_cards(data): out = [] for v, s in data.items(): dot = "up" if s["alive"] else "down" lat = s["latency"] lc = "lat-ok" if lat < 80 else ("lat-warn" if lat < 250 else "lat-bad") err = (s.get("error") or "OK")[:25] arts = "" for a in s.get("arts", []): arts += f'
' if not arts: arts = '