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:
LEGSTECH Optimizer
2026-04-25 12:20:30 +00:00
parent 746ab8fb11
commit c42345ea1e
4 changed files with 522 additions and 61 deletions
+67 -2
View File
@@ -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');
}