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() {
|
async function loadHistory() {
|
||||||
const r = await fetch('/api/runs');
|
var show = _historyView === 'archived' ? 'archived' : 'active';
|
||||||
const data = await r.json();
|
var r = await fetch('/api/runs?show=' + show);
|
||||||
|
var data = await r.json();
|
||||||
historyRuns = data.runs || [];
|
historyRuns = data.runs || [];
|
||||||
renderHistoryList();
|
renderHistoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function 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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
el.innerHTML = '<div class="hp-list">' + historyRuns.map(r => {
|
el.innerHTML = toggleBar + '<div class="hp-list">' + historyRuns.map(function(r) {
|
||||||
const d = new Date(r.created_at);
|
var d = new Date(r.created_at);
|
||||||
const time = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
var time = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
||||||
const models = (r.models_used || []).length;
|
var models = (r.models_used || []).length;
|
||||||
const prompt = (r.prompt || '').substring(0, 80);
|
var prompt = (r.prompt || '').substring(0, 80);
|
||||||
return `<div class="hp-item" onclick="viewRun(${r.id})">
|
return '<div class="hp-item" onclick="viewRun(' + r.id + ')">'
|
||||||
<div class="hp-mode">${r.mode}</div>
|
+ '<div class="hp-mode">' + r.mode + '</div>'
|
||||||
<div class="hp-prompt">${escapeHtml(prompt)}</div>
|
+ '<div class="hp-prompt">' + escapeHtml(prompt) + '</div>'
|
||||||
<div class="hp-meta"><span>${time}</span><span>${models} model${models!==1?'s':''}</span></div>
|
+ '<div class="hp-meta"><span>' + time + '</span><span>' + models + ' model' + (models!==1?'s':'') + '</span></div>'
|
||||||
</div>`;
|
+ '</div>';
|
||||||
}).join('') + '</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 style="font-size:13px;margin-bottom:8px">${escapeHtml(run.prompt)}</div>`;
|
||||||
html += `<div class="hp-actions">`;
|
html += `<div class="hp-actions">`;
|
||||||
html += `<button class="hp-btn" onclick="rerunFromHistory(${id})">Re-run</button>`;
|
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 += `<button class="hp-btn hp-btn-del" onclick="deleteRun(${id})">Delete</button>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
responses.forEach((resp, ri) => {
|
responses.forEach((resp, ri) => {
|
||||||
@ -2651,10 +2675,33 @@ async function rerunFromHistory(id) {
|
|||||||
toggleHistory();
|
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) {
|
async function deleteRun(id) {
|
||||||
await fetch('/api/runs/' + id, {method: 'DELETE'});
|
await fetch('/api/runs/' + id, {method: 'DELETE'});
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
renderHistoryList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── DEMO MODE ───────────────────────────────
|
// ─── DEMO MODE ───────────────────────────────
|
||||||
@ -4844,10 +4891,16 @@ setInterval(poll,3000);
|
|||||||
@app.route("/api/runs")
|
@app.route("/api/runs")
|
||||||
@login_required
|
@login_required
|
||||||
def get_runs():
|
def get_runs():
|
||||||
|
show = request.args.get("show", "active") # active, archived, all
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
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()
|
runs = cur.fetchall()
|
||||||
for r in runs:
|
for r in runs:
|
||||||
r["created_at"] = r["created_at"].isoformat()
|
r["created_at"] = r["created_at"].isoformat()
|
||||||
@ -4885,6 +4938,60 @@ def delete_run(run_id):
|
|||||||
return jsonify({"error": str(e)}), 500
|
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")
|
@app.route("/api/pipelines")
|
||||||
@login_required
|
@login_required
|
||||||
def get_pipelines():
|
def get_pipelines():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user