diff --git a/app.py b/app.py index df1cf04..c8397c7 100644 --- a/app.py +++ b/app.py @@ -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(): """ diff --git a/optimizer/pipeline.py b/optimizer/pipeline.py index ae2a847..62aa428 100644 --- a/optimizer/pipeline.py +++ b/optimizer/pipeline.py @@ -96,6 +96,13 @@ class OptimizationPipeline: # All completed runs (for history restoration) self._completed_runs: list[dict] = [] + # Live activity logs — persisted in-memory so a dashboard refresh during + # a run can replay them via /api/thinking, /api/param_changes, /api/validation. + self._thinking_log: list[dict] = [] + self._param_changes: list[dict] = [] + self._validation_log: list[dict] = [] + self._early_term: Optional[dict] = None + # ── Public API ──────────────────────────────────────────────────────────── def configure(self, session: SessionConfig) -> None: @@ -187,6 +194,11 @@ class OptimizationPipeline: self._ai_insights.clear() self._run_findings.clear() self._run_insights.clear() + # Reset live-activity logs for the new run + self._thinking_log = [] + self._param_changes = [] + self._validation_log = [] + self._early_term = None budget.start() @@ -1013,6 +1025,10 @@ class OptimizationPipeline: } if meta: payload["meta"] = meta + # Persist for dashboard refresh — keep last 200 entries + self._thinking_log.append(payload) + if len(self._thinking_log) > 200: + self._thinking_log = self._thinking_log[-200:] self._emit("ai_thinking", payload) def _emit_early_termination(self, reason_code: str, message: str, details: dict = None) -> None: @@ -1033,9 +1049,23 @@ class OptimizationPipeline: } if details: payload["details"] = details + self._early_term = payload # remember for refresh self._emit("early_termination", payload) def _emit(self, event: str, data: dict = {}) -> None: + # Tee select live-activity events into in-memory logs so a dashboard + # refresh during the run can replay them via REST. + try: + if event == "param_changes": + self._param_changes.append(data) + if len(self._param_changes) > 50: + self._param_changes = self._param_changes[-50:] + elif event in ("validation_start", "validation_run_start", "validation_run_complete", "validation_done"): + self._validation_log.append({"event": event, **data}) + if len(self._validation_log) > 40: + self._validation_log = self._validation_log[-40:] + except Exception: + pass try: self.socketio.emit(event, data) except Exception as e: diff --git a/ui/static/js/dashboard.js b/ui/static/js/dashboard.js index f433b78..12268ef 100644 --- a/ui/static/js/dashboard.js +++ b/ui/static/js/dashboard.js @@ -1043,12 +1043,22 @@ function renderThinkingFeed() { } function addThinking(d) { + // For replayed events, use the original ts from the server. For live ones, use now. + let timeLabel = nowStr(); + if (d.ts) { + try { + const dt = new Date(d.ts); + if (!isNaN(dt)) { + timeLabel = dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } + } catch (_) {} + } const entry = { msg: d.msg || '', kind: d.kind || 'info', iteration: d.iteration, phase: d.phase, - time: nowStr(), + time: timeLabel, }; thinkingFeedState.entries.push(entry); if (thinkingFeedState.entries.length > MAX_THINKING_ENTRIES) { @@ -1324,9 +1334,10 @@ safe('btn-stop', stopBtn => { async function restoreHistory() { try { - const [histResp, aiResp] = await Promise.all([ + const [histResp, aiResp, liveResp] = await Promise.all([ fetch('/api/history'), fetch('/api/ai_insight/latest'), + fetch('/api/live_activity'), ]); const runs = await histResp.json(); @@ -1366,6 +1377,60 @@ async function restoreHistory() { updateAIPanel(insight); addLog('AI insight restored (run: ' + (insight.run_id || '?') + ').', 'info'); } + + // ── Restore Live Intelligence (thinking feed, param changes, validation, banner) ── + // This is what makes a refresh-mid-run feel like nothing happened. + const live = await liveResp.json(); + if (live) { + // Thinking feed + const thinking = Array.isArray(live.thinking) ? live.thinking : []; + thinking.forEach(t => addThinking(t)); + + // Param changes + const pcs = Array.isArray(live.param_changes) ? live.param_changes : []; + pcs.forEach(pc => addParamChanges(pc)); + + // Validation panel + const val = Array.isArray(live.validation) ? live.validation : []; + val.forEach(v => { + if (v.event === 'validation_start') { + validationState.runs = []; + validationState.planned = v.planned_runs || 0; + validationState.complete = 0; + validationState.active = true; + } else if (v.event === 'validation_run_start') { + validationRunStart(v); + } else if (v.event === 'validation_run_complete') { + validationRunComplete(v); + } else if (v.event === 'validation_done') { + validationDone(v); + } + }); + if (val.length) renderValidation(); + + // Early-termination banner + if (live.early_termination) { + showEarlyTermination(live.early_termination); + } + + // Phase tracker — restore active phase even if no events have arrived yet + const phaseMapLocal = { phase1: 1, phase2: 2, phase2_ai: 2, phase3: 3, phase3_oos: 3, phase3_sens: 3 }; + const ph = phaseMapLocal[live.phase] || (live.running ? 1 : 0); + if (ph) { + setPhaseActive(ph, { mode: live.phase_mode }); + } + if (live.running) { + setRunningState(true, live.phase || 'Optimizing...'); + } + + if (thinking.length || pcs.length || val.length) { + addLog( + `Restored live activity: ${thinking.length} thinking, ` + + `${pcs.length} param-change iterations, ${val.length} validation events.`, + 'info' + ); + } + } } catch (e) { addLog('History restore: ' + e.message, 'warn'); } diff --git a/ui/templates/reports_index.html b/ui/templates/reports_index.html index 55f7e8f..8aa7eaf 100644 --- a/ui/templates/reports_index.html +++ b/ui/templates/reports_index.html @@ -2,92 +2,408 @@
-