Archive/restore history: soft-delete with toggle and bulk ops
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) <noreply@anthropic.com>
This commit is contained in:
parent
7948089f04
commit
aeab1f0194
139
llm_team_ui.py
139
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 = '<div style="display:flex;gap:4px;padding:8px;border-bottom:2px solid var(--border)">'
|
||||
+ '<button class="hp-btn" style="'+ (!isArchived ? 'border-color:var(--accent);color:var(--accent)' : '') +'" onclick="_historyView=\'active\';loadHistory()">Active</button>'
|
||||
+ '<button class="hp-btn" style="'+ (isArchived ? 'border-color:var(--accent);color:var(--accent)' : '') +'" onclick="_historyView=\'archived\';loadHistory()">Archived</button>'
|
||||
+ '<span style="flex:1"></span>';
|
||||
if (!isArchived && historyRuns.length > 0) {
|
||||
toggleBar += '<button class="hp-btn" style="border-color:rgba(217,70,239,0.3);color:#d946ef;font-size:9px" onclick="bulkArchive()">Archive All</button>';
|
||||
}
|
||||
if (isArchived && historyRuns.length > 0) {
|
||||
toggleBar += '<button class="hp-btn" style="border-color:rgba(74,222,128,0.3);color:var(--green);font-size:9px" onclick="bulkRestore()">Restore All</button>';
|
||||
}
|
||||
toggleBar += '</div>';
|
||||
|
||||
if (!historyRuns.length) {
|
||||
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text2)">No runs saved yet. Run a team to see history here.</div>';
|
||||
el.innerHTML = toggleBar + '<div style="text-align:center;padding:40px;color:var(--text2);font-family:JetBrains Mono,monospace;font-size:11px">' + (isArchived ? 'No archived runs.' : 'No active runs. Run a team to see history here.') + '</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = '<div class="hp-list">' + 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 `<div class="hp-item" onclick="viewRun(${r.id})">
|
||||
<div class="hp-mode">${r.mode}</div>
|
||||
<div class="hp-prompt">${escapeHtml(prompt)}</div>
|
||||
<div class="hp-meta"><span>${time}</span><span>${models} model${models!==1?'s':''}</span></div>
|
||||
</div>`;
|
||||
el.innerHTML = toggleBar + '<div class="hp-list">' + 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 '<div class="hp-item" onclick="viewRun(' + r.id + ')">'
|
||||
+ '<div class="hp-mode">' + r.mode + '</div>'
|
||||
+ '<div class="hp-prompt">' + escapeHtml(prompt) + '</div>'
|
||||
+ '<div class="hp-meta"><span>' + time + '</span><span>' + models + ' model' + (models!==1?'s':'') + '</span></div>'
|
||||
+ '</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
@ -2625,6 +2643,12 @@ async function viewRun(id) {
|
||||
html += `<div style="font-size:13px;margin-bottom:8px">${escapeHtml(run.prompt)}</div>`;
|
||||
html += `<div class="hp-actions">`;
|
||||
html += `<button class="hp-btn" onclick="rerunFromHistory(${id})">Re-run</button>`;
|
||||
var isArch = run.archived;
|
||||
if (isArch) {
|
||||
html += `<button class="hp-btn" style="border-color:rgba(74,222,128,0.3);color:var(--green)" onclick="restoreRun(${id})">Restore</button>`;
|
||||
} else {
|
||||
html += `<button class="hp-btn" style="border-color:rgba(217,70,239,0.3);color:#d946ef" onclick="archiveRun(${id})">Archive</button>`;
|
||||
}
|
||||
html += `<button class="hp-btn hp-btn-del" onclick="deleteRun(${id})">Delete</button>`;
|
||||
html += `</div>`;
|
||||
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/<int:run_id>/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/<int:run_id>/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():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user