Auto-save self-analysis reports to DB with browsable history

Database:
- self_reports table: report_type, model, report text, data_size, timestamp
- Reports auto-saved on generation (no extra step needed)

API:
- GET /api/self-reports — list all past reports (id, type, model, size, date)
- GET /api/self-reports/:id — full report text

UI:
- "✓ Saved as report #N" indicator after generation
- "Past Reports (N)" section below self-analysis buttons
- Click any past report → expands inline (toggle on/off)
- Shows: type, model, timestamp for each saved report
- Reports persist across page refreshes and restarts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-26 04:49:17 -05:00
parent 28e641f939
commit 804898b658

View File

@ -3388,6 +3388,7 @@ LAB_HTML = r"""
<div class="card" style="border-color:rgba(74,222,128,0.2)">
<h3 style="color:var(--green)">Self-Analysis <span style="font-size:9px;color:var(--text2);font-weight:400;text-transform:none;letter-spacing:0">AI reports from your own system data</span></h3>
<div style="display:grid;gap:8px" id="self-reports"></div>
<div id="past-reports" style="margin-top:12px"></div>
</div>
<div class="card" id="templates-card">
<h3>Experiment Templates <span style="font-size:9px;color:var(--text2);font-weight:400;text-transform:none;letter-spacing:0">click to auto-fill the create form</span></h3>
@ -3866,14 +3867,84 @@ async function runSelfReport(type, card) {
content.textContent = d.report;
panel.appendChild(content);
card.parentNode.insertBefore(panel, card.nextSibling);
toast('Report generated', true);
if (d.id) {
var saved = document.createElement('div');
saved.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--green);margin-top:8px;text-transform:uppercase;letter-spacing:1px';
saved.textContent = '✓ Saved as report #' + d.id;
panel.appendChild(saved);
}
toast('Report generated & saved', true);
loadPastReports();
} catch(e) { toast('Error: '+e.message, false); }
btn.textContent = 'Run →';
btn.style.color = 'var(--green)';
card.style.borderColor = origBorder;
}
async function loadPastReports() {
var el = document.getElementById('past-reports');
if (!el) return;
try {
var r = await fetch('/api/self-reports');
var d = await r.json();
var reports = d.reports || [];
if (!reports.length) { el.textContent = ''; return; }
el.textContent = '';
var title = document.createElement('div');
title.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:8px;text-transform:uppercase;letter-spacing:2px;color:var(--text2);margin-bottom:8px;padding-top:8px;border-top:1px solid var(--border)';
title.textContent = 'Past Reports (' + reports.length + ')';
el.appendChild(title);
reports.forEach(function(rpt) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid rgba(42,45,53,0.3);cursor:pointer;font-size:11px';
row.onmouseenter = function(){row.style.background='rgba(74,222,128,0.03)'};
row.onmouseleave = function(){row.style.background='transparent'};
var typeEl = document.createElement('span');
typeEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;color:var(--green);font-weight:700;min-width:130px';
typeEl.textContent = rpt.report_type.replace(/_/g,' ');
var modelEl = document.createElement('span');
modelEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--text2);min-width:90px';
modelEl.textContent = rpt.model;
var dateEl = document.createElement('span');
dateEl.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;color:var(--text2);margin-left:auto';
dateEl.textContent = new Date(rpt.created_at).toLocaleString();
row.appendChild(typeEl); row.appendChild(modelEl); row.appendChild(dateEl);
row.onclick = function(){viewPastReport(rpt.id)};
el.appendChild(row);
});
} catch(e) {}
}
async function viewPastReport(id) {
var resultId = 'report-past-' + id;
var existing = document.getElementById(resultId);
if (existing) { existing.remove(); return; }
try {
var r = await fetch('/api/self-reports/' + id);
var d = await r.json();
if (d.error) return;
var panel = document.createElement('div');
panel.id = resultId;
panel.style.cssText = 'background:rgba(0,0,0,0.3);border:2px solid var(--green);border-radius:2px;padding:16px;margin:8px 0;max-height:500px;overflow-y:auto';
var title = document.createElement('div');
title.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--green);margin-bottom:8px;display:flex;justify-content:space-between';
title.textContent = d.report_type.replace(/_/g,' ') + '' + d.model + '' + new Date(d.created_at).toLocaleString();
var closeBtn = document.createElement('span');
closeBtn.style.cssText = 'cursor:pointer;opacity:0.6';
closeBtn.textContent = '';
closeBtn.onclick = function(e){e.stopPropagation();panel.remove()};
title.appendChild(closeBtn);
panel.appendChild(title);
var content = document.createElement('div');
content.style.cssText = 'font-size:12px;line-height:1.7;white-space:pre-wrap;color:var(--text)';
content.textContent = d.report;
panel.appendChild(content);
document.getElementById('past-reports').appendChild(panel);
} catch(e) {}
}
renderSelfReports();
loadPastReports();
function renderTemplates() {
var el = document.getElementById('template-list');
@ -5689,7 +5760,50 @@ def self_analyze():
except Exception as e:
return jsonify({"error": str(e)}), 500
return jsonify({"report": report, "type": report_type, "model": model, "data_size": len(context)})
# Save to DB
report_id = None
try:
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("INSERT INTO self_reports (report_type, model, report, data_size) VALUES (%s,%s,%s,%s) RETURNING id",
(report_type, model, report, len(context)))
report_id = cur.fetchone()[0]
conn.commit()
except Exception:
pass
return jsonify({"report": report, "type": report_type, "model": model, "data_size": len(context), "id": report_id})
@app.route("/api/self-reports")
@admin_required
def list_self_reports():
try:
with get_db() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT id, report_type, model, LENGTH(report) as report_len, data_size, created_at FROM self_reports ORDER BY created_at DESC LIMIT 50")
rows = cur.fetchall()
for r in rows:
r["created_at"] = r["created_at"].isoformat()
return jsonify({"reports": rows})
except Exception as e:
return jsonify({"reports": [], "error": str(e)})
@app.route("/api/self-reports/<int:rid>")
@admin_required
def get_self_report(rid):
try:
with get_db() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM self_reports WHERE id = %s", (rid,))
row = cur.fetchone()
if not row:
return jsonify({"error": "not found"}), 404
row["created_at"] = row["created_at"].isoformat()
return jsonify(row)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/pipelines")