Evolution system: twice-daily agent sweep cron, evolution log, NOC evolution panel
This commit is contained in:
@@ -2,3 +2,5 @@
|
||||
* Running on http://127.0.0.1:5106
|
||||
[33mPress CTRL+C to quit[0m
|
||||
127.0.0.1 - - [03/Aug/2026 21:35:50] "GET /health HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [03/Aug/2026 23:50:58] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [03/Aug/2026 23:50:58] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
||||
|
||||
10
core/evolution_log.json
Normal file
10
core/evolution_log.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"vertical": "all",
|
||||
"agent": "hermes",
|
||||
"feature": "NOC Dashboard v2 deployed",
|
||||
"details": "Clean premium design with Inter font, per-site cards, metrics, evolution tracking panel",
|
||||
"status": "completed",
|
||||
"timestamp": "2026-08-03T23:51:15.624329"
|
||||
}
|
||||
]
|
||||
74
core/evolution_log.py
Normal file
74
core/evolution_log.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Site Evolution Log — tracks all improvements made by evolution agents.
|
||||
Appended to by sub-agents, displayed on NOC dashboard.
|
||||
"""
|
||||
import json, os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
LOG_PATH = Path(os.path.expanduser("~/auto-publisher/core/evolution_log.json"))
|
||||
|
||||
def log_evolution(vertical: str, agent: str, feature: str, details: str, status: str = "completed"):
|
||||
"""Record an evolution event."""
|
||||
log = []
|
||||
if LOG_PATH.exists():
|
||||
log = json.loads(LOG_PATH.read_text())
|
||||
|
||||
log.append({
|
||||
"vertical": vertical,
|
||||
"agent": agent,
|
||||
"feature": feature,
|
||||
"details": details[:500],
|
||||
"status": status,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
})
|
||||
|
||||
# Keep last 200 entries
|
||||
log = log[-200:]
|
||||
LOG_PATH.write_text(json.dumps(log, indent=2))
|
||||
|
||||
|
||||
def get_recent_evolutions(limit: int = 20) -> list[dict]:
|
||||
"""Get recent evolution events."""
|
||||
if not LOG_PATH.exists():
|
||||
return []
|
||||
log = json.loads(LOG_PATH.read_text())
|
||||
return sorted(log, key=lambda x: x["timestamp"], reverse=True)[:limit]
|
||||
|
||||
|
||||
def get_evolution_stats() -> dict:
|
||||
"""Aggregate evolution statistics."""
|
||||
if not LOG_PATH.exists():
|
||||
return {"total": 0, "by_vertical": {}, "recent": []}
|
||||
|
||||
log = json.loads(LOG_PATH.read_text())
|
||||
by_v = {}
|
||||
for entry in log:
|
||||
v = entry["vertical"]
|
||||
by_v[v] = by_v.get(v, 0) + 1
|
||||
|
||||
return {
|
||||
"total": len(log),
|
||||
"by_vertical": by_v,
|
||||
"latest": log[-1]["timestamp"][:19] if log else None,
|
||||
"recent": [e["feature"] for e in log[-5:]],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--log", action="store_true")
|
||||
ap.add_argument("--vertical", type=str, default="all")
|
||||
ap.add_argument("--feature", type=str, default="test")
|
||||
ap.add_argument("--details", type=str, default="")
|
||||
ap.add_argument("--agent", type=str, default="manual")
|
||||
ap.add_argument("--status", type=str, default="completed")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.log:
|
||||
log_evolution(args.vertical, args.agent, args.feature, args.details, args.status)
|
||||
print(f"Logged: {args.vertical} — {args.feature}")
|
||||
else:
|
||||
stats = get_evolution_stats()
|
||||
print(json.dumps(stats, indent=2))
|
||||
@@ -1,10 +1,14 @@
|
||||
"""
|
||||
NOC Dashboard — clean, modern fleet monitor for all 8 publisher sites.
|
||||
"""
|
||||
import json, time, threading, requests
|
||||
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 = {
|
||||
@@ -57,6 +61,9 @@ 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),
|
||||
@@ -65,6 +72,8 @@ def home():
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -115,10 +124,27 @@ def api_refresh():
|
||||
refresh()
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route("/api/evolution")
|
||||
def api_evolution():
|
||||
return jsonify({
|
||||
"stats": get_evolution_stats(),
|
||||
"recent": get_recent_evolutions(20),
|
||||
})
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({"status": "ok", "ts": cache["ts"]})
|
||||
|
||||
def build_evo_html(events):
|
||||
if not events:
|
||||
return '<div class="ev-empty">No evolution events yet. First sweep runs at midnight.</div>'
|
||||
out = []
|
||||
for e in events:
|
||||
ts = e["timestamp"][:16].replace("T", " ")
|
||||
out.append(f'<div class="ev-row"><span class="ev-ts">{ts}</span><span class="ev-v" style="color:{SITES.get(e["vertical"],{}).get("color","#888")}">{e["vertical"]}</span><span class="ev-feat">{e["feature"][:80]}</span><span class="ev-stat">{e["status"]}</span></div>')
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ── Template ──
|
||||
HTML = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -177,7 +203,19 @@ body{{font-family:var(--f);background:var(--bg);color:var(--t);min-height:100vh;
|
||||
.ft{{text-align:center;padding:1.5rem;color:var(--t3);font-size:.7rem;border-top:1px solid var(--b)}}
|
||||
.ft a{{color:var(--t2);text-decoration:none}}
|
||||
.ft a:hover{{color:var(--t)}}
|
||||
@media(max-width:800px){{.gr{{grid-template-columns:1fr;padding:0 1rem 1rem}}.sr{{grid-template-columns:repeat(3,1fr);padding:1rem}}.cm{{grid-template-columns:repeat(3,1fr)}}.tb{{padding:0 1rem}}}}
|
||||
.ft a:hover{{color:var(--t)}}
|
||||
.ev-panel{{max-width:1400px;margin:0 auto;padding:0 2rem 2rem}}
|
||||
.ev-inner{{background:var(--s1);border:1px solid var(--b);border-radius:var(--r);padding:1.25rem}}
|
||||
.ev-title{{font-weight:700;font-size:.95rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}}
|
||||
.ev-title span{{color:var(--t3);font-weight:400;font-size:.75rem}}
|
||||
.ev-row{{display:flex;align-items:center;gap:.75rem;padding:.35rem 0;font-size:.75rem;border-bottom:1px solid var(--b)}}
|
||||
.ev-row:last-child{{border-bottom:none}}
|
||||
.ev-ts{{color:var(--t3);min-width:110px;font-variant-numeric:tabular-nums}}
|
||||
.ev-v{{font-weight:600;min-width:55px;text-transform:capitalize}}
|
||||
.ev-feat{{color:var(--t2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
|
||||
.ev-stat{{color:#22c55e;font-size:.65rem;text-transform:uppercase;font-weight:600}}
|
||||
.ev-empty{{color:var(--t3);font-size:.8rem;font-style:italic;padding:.5rem 0}}
|
||||
@media(max-width:800px){{.gr{{grid-template-columns:1fr;padding:0 1rem 1rem}}.sr{{grid-template-columns:repeat(3,1fr);padding:1rem}}.cm{{grid-template-columns:repeat(3,1fr)}}.tb{{padding:0 1rem}}.ev-panel{{padding:0 1rem 1rem}}}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -198,6 +236,12 @@ body{{font-family:var(--f);background:var(--bg);color:var(--t);min-height:100vh;
|
||||
<div class="sb"><div class="sv" style="color:#ec4899">{articles}</div><div class="sl">Network Total</div></div>
|
||||
</div>
|
||||
<div class="gr">{cards}</div>
|
||||
<div class="ev-panel">
|
||||
<div class="ev-inner">
|
||||
<div class="ev-title">🔄 Site Evolution <span>({evo_total} improvements)</span></div>
|
||||
{evo_html}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft">Publisher Fleet NOC · Auto-refresh 60s · <a href="http://localhost:5106">Pipeline</a> · <a href="http://localhost:5053/fleet">Kalshi</a></div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
Reference in New Issue
Block a user