Evolution system: twice-daily agent sweep cron, evolution log, NOC evolution panel

This commit is contained in:
drjones
2026-08-03 23:51:36 -07:00
parent 2e2d497cbd
commit 7201804a31
4 changed files with 132 additions and 2 deletions

View File

@@ -2,3 +2,5 @@
* Running on http://127.0.0.1:5106 * Running on http://127.0.0.1:5106
Press CTRL+C to quit Press CTRL+C to quit
127.0.0.1 - - [03/Aug/2026 21:35:50] "GET /health HTTP/1.1" 200 - 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] "GET /favicon.ico HTTP/1.1" 404 -

10
core/evolution_log.json Normal file
View 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
View 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))

View File

@@ -1,10 +1,14 @@
""" """
NOC Dashboard — clean, modern fleet monitor for all 8 publisher sites. 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 datetime import datetime
from flask import Flask, jsonify 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__) app = Flask(__name__)
SITES = { SITES = {
@@ -57,6 +61,9 @@ def home():
if not cache["ts"] or (datetime.now() - datetime.fromisoformat(cache["ts"])).seconds > 30: if not cache["ts"] or (datetime.now() - datetime.fromisoformat(cache["ts"])).seconds > 30:
threading.Thread(target=refresh, daemon=True).start() threading.Thread(target=refresh, daemon=True).start()
data = cache["data"] or {} 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( return HTML.format(
alive=sum(1 for s in data.values() if s.get("alive")), alive=sum(1 for s in data.values() if s.get("alive")),
total=len(data), total=len(data),
@@ -65,6 +72,8 @@ def home():
today=sum(s.get("today", 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()), subs=sum(s.get("subs", 0) for s in data.values()),
updated=(cache["ts"] or "")[:19], updated=(cache["ts"] or "")[:19],
evo_total=evo_stats["total"],
evo_html=evo_html,
cards=build_cards(data), cards=build_cards(data),
) )
@@ -115,10 +124,27 @@ def api_refresh():
refresh() refresh()
return jsonify({"status": "ok"}) 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") @app.route("/health")
def health(): def health():
return jsonify({"status": "ok", "ts": cache["ts"]}) 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 ── # ── Template ──
HTML = """<!DOCTYPE html> HTML = """<!DOCTYPE html>
<html lang="en"> <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{{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{{color:var(--t2);text-decoration:none}}
.ft a:hover{{color:var(--t)}} .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> </style>
</head> </head>
<body> <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 class="sb"><div class="sv" style="color:#ec4899">{articles}</div><div class="sl">Network Total</div></div>
</div> </div>
<div class="gr">{cards}</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> <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> </body>
</html>""" </html>"""