fix: live-activity persistence on refresh + reports page rebuild
Refresh-during-run no longer wipes the dashboard - pipeline now keeps in-memory rolling logs of ai_thinking, param_changes, validation events, and the current early-termination state. Logs reset at the start of each new run (capped: 200 thinking / 50 param-change records / 40 validation events) - new GET /api/live_activity returns those logs + current phase + running state in one shot — the dashboard hits it on page load - restoreHistory() in dashboard.js now replays each event into addThinking / addParamChanges / validationRunStart-Complete-Done / showEarlyTermination, and re-applies the active phase via setPhaseActive. Reload F5 mid-run no longer shows a fresh empty dashboard - addThinking() preserves the original ts on replay (was using nowStr() so every replayed entry got the refresh time) Reports page (/reports) rebuilt - previous template crashed with 500 on legacy summary.json files that pre-date the win_rate / drawdown_pct fields. Server now backfills sane defaults for every metric the template touches - runs now sorted by ts (was filesystem-iterdir order) - new template: search bar + filter chips (All / Exploration / AI Iteration / Validation / AI insight / Has .set), phase tags, AI/.set badges, three action buttons per card (View Params / Download .set / Full Report) - per-card detail modal shows metrics grid + full parameters table + AI reasoning + Download .set button — the missing "click to see params + download" path the user reported - has_set detected per-run by globbing run_dir/*.set; AI insight tag shown when ai_insight.json exists on disk
This commit is contained in:
@@ -186,21 +186,38 @@ def download_set(run_id):
|
||||
def reports_index():
|
||||
import json, re
|
||||
runs = []
|
||||
for run_dir in sorted(REPORTS_DIR.iterdir(), reverse=True) if REPORTS_DIR.exists() else []:
|
||||
if not run_dir.is_dir():
|
||||
continue
|
||||
summary = run_dir / "summary.json"
|
||||
if summary.exists():
|
||||
if REPORTS_DIR.exists():
|
||||
for run_dir in REPORTS_DIR.iterdir():
|
||||
if not run_dir.is_dir():
|
||||
continue
|
||||
summary = run_dir / "summary.json"
|
||||
if not summary.exists():
|
||||
continue
|
||||
try:
|
||||
txt = summary.read_text(encoding="utf-8")
|
||||
txt = re.sub(r'\bNaN\b', 'null', txt)
|
||||
txt = re.sub(r'\bInfinity\b', 'null', txt)
|
||||
data = json.loads(txt)
|
||||
data["score"] = data.get("score") or 0
|
||||
data["score_delta"] = data.get("score_delta") or 0
|
||||
# Defensive defaults so the template never crashes on legacy files.
|
||||
data["run_id"] = data.get("run_id") or run_dir.name
|
||||
data["score"] = data.get("score") or 0
|
||||
data["score_delta"] = data.get("score_delta") or 0
|
||||
data["net_profit"] = data.get("net_profit") or 0
|
||||
data["profit_factor"] = data.get("profit_factor") or 0
|
||||
data["calmar"] = data.get("calmar") or 0
|
||||
data["drawdown_pct"] = data.get("drawdown_pct") or 0
|
||||
data["win_rate"] = data.get("win_rate") or 0
|
||||
data["total_trades"] = data.get("total_trades") or 0
|
||||
data["ts"] = data.get("ts") or ""
|
||||
data["phase"] = data.get("phase") or "phase1"
|
||||
# Surface per-card flags the template uses
|
||||
data["has_set"] = bool(list(run_dir.glob("*.set")))
|
||||
data["has_ai"] = (run_dir / "ai_insight.json").exists() or bool(data.get("has_ai"))
|
||||
runs.append(data)
|
||||
except Exception:
|
||||
pass
|
||||
# Sort by ts desc — newest first (was relying on filesystem sort order before)
|
||||
runs.sort(key=lambda r: r.get("ts", ""), reverse=True)
|
||||
return render_template("reports_index.html", runs=runs[:100])
|
||||
|
||||
|
||||
@@ -273,6 +290,39 @@ def run_detail(run_id):
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/live_activity")
|
||||
def live_activity():
|
||||
"""
|
||||
Single endpoint the dashboard hits on (re)connect to restore everything
|
||||
that's not already in /api/history: AI thinking feed, parameter changes,
|
||||
validation runs, early-termination state, current phase + mode.
|
||||
"""
|
||||
if not pipeline:
|
||||
return jsonify({
|
||||
"thinking": [], "param_changes": [], "validation": [],
|
||||
"early_termination": None,
|
||||
"phase": "idle", "phase_mode": None,
|
||||
"running": False,
|
||||
})
|
||||
|
||||
# Determine phase mode (autonomous?) for label hints
|
||||
phase_mode = None
|
||||
if getattr(pipeline, "session", None):
|
||||
phase_mode = "autonomous" if getattr(pipeline.session, "autonomous_mode", False) else None
|
||||
|
||||
return jsonify({
|
||||
"thinking": getattr(pipeline, "_thinking_log", []) or [],
|
||||
"param_changes": getattr(pipeline, "_param_changes", []) or [],
|
||||
"validation": getattr(pipeline, "_validation_log", []) or [],
|
||||
"early_termination": getattr(pipeline, "_early_term", None),
|
||||
"phase": getattr(pipeline, "_phase", "idle"),
|
||||
"phase_mode": phase_mode,
|
||||
"running": bool(getattr(pipeline, "running", False)),
|
||||
"run_count": getattr(pipeline, "_run_count", 0),
|
||||
"total_runs": getattr(pipeline, "_total_runs", 0),
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/best_result")
|
||||
def best_result():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user