From aeab1f0194fc077a807ace74dc2a26639ecf5c2c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 04:13:33 -0500 Subject: [PATCH] Archive/restore history: soft-delete with toggle and bulk ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Database: - Added 'archived' boolean column to team_runs (indexed) - Active runs filtered by archived=false by default API: - GET /api/runs?show=active|archived|all - POST /api/runs/:id/archive — archive single run - POST /api/runs/:id/restore — restore single run - POST /api/runs/bulk-archive — archive/restore by IDs or date History panel UI: - Active/Archived toggle tabs at top - Per-run Archive button (magenta) in detail view - Per-run Restore button (green) in detail view for archived runs - "Archive All" bulk button when viewing active runs - "Restore All" bulk button when viewing archived runs - Archived runs hidden from active view, accessible anytime Co-Authored-By: Claude Opus 4.6 (1M context) --- llm_team_ui.py | 139 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 16 deletions(-) diff --git a/llm_team_ui.py b/llm_team_ui.py index 82f7f20..7233526 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -2587,29 +2587,47 @@ function toggleHistory() { } } +var _historyView = 'active'; // active or archived + async function loadHistory() { - const r = await fetch('/api/runs'); - const data = await r.json(); + var show = _historyView === 'archived' ? 'archived' : 'active'; + var r = await fetch('/api/runs?show=' + show); + var data = await r.json(); historyRuns = data.runs || []; renderHistoryList(); } function renderHistoryList() { - const el = document.getElementById('hp-content'); + var el = document.getElementById('hp-content'); + var isArchived = _historyView === 'archived'; + + // Toggle bar + var toggleBar = '
' + + '' + + '' + + ''; + if (!isArchived && historyRuns.length > 0) { + toggleBar += ''; + } + if (isArchived && historyRuns.length > 0) { + toggleBar += ''; + } + toggleBar += '
'; + if (!historyRuns.length) { - el.innerHTML = '
No runs saved yet. Run a team to see history here.
'; + el.innerHTML = toggleBar + '
' + (isArchived ? 'No archived runs.' : 'No active runs. Run a team to see history here.') + '
'; return; } - el.innerHTML = '
' + historyRuns.map(r => { - const d = new Date(r.created_at); - const time = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); - const models = (r.models_used || []).length; - const prompt = (r.prompt || '').substring(0, 80); - return `
-
${r.mode}
-
${escapeHtml(prompt)}
-
${time}${models} model${models!==1?'s':''}
-
`; + el.innerHTML = toggleBar + '
' + historyRuns.map(function(r) { + var d = new Date(r.created_at); + var time = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); + var models = (r.models_used || []).length; + var prompt = (r.prompt || '').substring(0, 80); + return '
' + + '
' + r.mode + '
' + + '
' + escapeHtml(prompt) + '
' + + '
' + time + '' + models + ' model' + (models!==1?'s':'') + '
' + + '
'; }).join('') + '
'; } @@ -2625,6 +2643,12 @@ async function viewRun(id) { html += `
${escapeHtml(run.prompt)}
`; html += `
`; html += ``; + var isArch = run.archived; + if (isArch) { + html += ``; + } else { + html += ``; + } html += ``; html += `
`; responses.forEach((resp, ri) => { @@ -2651,10 +2675,33 @@ async function rerunFromHistory(id) { toggleHistory(); } +async function archiveRun(id) { + await fetch('/api/runs/' + id + '/archive', {method: 'POST'}); + await loadHistory(); +} + +async function restoreRun(id) { + await fetch('/api/runs/' + id + '/restore', {method: 'POST'}); + await loadHistory(); +} + +async function bulkArchive() { + if (!confirm('Archive all ' + historyRuns.length + ' active runs?')) return; + var ids = historyRuns.map(function(r) { return r.id; }); + await fetch('/api/runs/bulk-archive', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({action: 'archive', ids: ids})}); + await loadHistory(); +} + +async function bulkRestore() { + if (!confirm('Restore all ' + historyRuns.length + ' archived runs?')) return; + var ids = historyRuns.map(function(r) { return r.id; }); + await fetch('/api/runs/bulk-archive', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({action: 'restore', ids: ids})}); + await loadHistory(); +} + async function deleteRun(id) { await fetch('/api/runs/' + id, {method: 'DELETE'}); await loadHistory(); - renderHistoryList(); } // ─── DEMO MODE ─────────────────────────────── @@ -4844,10 +4891,16 @@ setInterval(poll,3000); @app.route("/api/runs") @login_required def get_runs(): + show = request.args.get("show", "active") # active, archived, all try: with get_db() as conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - cur.execute("SELECT id, mode, prompt, models_used, created_at FROM team_runs ORDER BY created_at DESC LIMIT 50") + if show == "archived": + cur.execute("SELECT id, mode, prompt, models_used, created_at, archived FROM team_runs WHERE archived = true ORDER BY created_at DESC LIMIT 200") + elif show == "all": + cur.execute("SELECT id, mode, prompt, models_used, created_at, archived FROM team_runs ORDER BY created_at DESC LIMIT 200") + else: + cur.execute("SELECT id, mode, prompt, models_used, created_at, archived FROM team_runs WHERE archived = false ORDER BY created_at DESC LIMIT 50") runs = cur.fetchall() for r in runs: r["created_at"] = r["created_at"].isoformat() @@ -4885,6 +4938,60 @@ def delete_run(run_id): return jsonify({"error": str(e)}), 500 +@app.route("/api/runs//archive", methods=["POST"]) +@login_required +def archive_run(run_id): + try: + with get_db() as conn: + with conn.cursor() as cur: + cur.execute("UPDATE team_runs SET archived = true WHERE id = %s", (run_id,)) + conn.commit() + return jsonify({"ok": True}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/runs//restore", methods=["POST"]) +@login_required +def restore_run(run_id): + try: + with get_db() as conn: + with conn.cursor() as cur: + cur.execute("UPDATE team_runs SET archived = false WHERE id = %s", (run_id,)) + conn.commit() + return jsonify({"ok": True}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/runs/bulk-archive", methods=["POST"]) +@admin_required +def bulk_archive_runs(): + data = request.json or {} + action = data.get("action", "archive") # archive or restore + ids = data.get("ids", []) + before = data.get("before") # archive all before this date + try: + with get_db() as conn: + with conn.cursor() as cur: + archived_val = action == "archive" + if ids: + cur.execute("UPDATE team_runs SET archived = %s WHERE id = ANY(%s)", (archived_val, ids)) + count = cur.rowcount + elif before: + cur.execute("UPDATE team_runs SET archived = %s WHERE created_at < %s AND archived = %s", + (archived_val, before, not archived_val)) + count = cur.rowcount + else: + # Archive all + cur.execute("UPDATE team_runs SET archived = true WHERE archived = false") + count = cur.rowcount + conn.commit() + return jsonify({"ok": True, "count": count}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/api/pipelines") @login_required def get_pipelines():