diff --git a/backend/app/api.py b/backend/app/api.py
index 896756f..80c6992 100644
--- a/backend/app/api.py
+++ b/backend/app/api.py
@@ -113,7 +113,9 @@ def admin_credit(member_id:UUID,incoming:CreditGrant,actor=Depends(administrator
@app.get('/admin/evaluations')
def admin_jobs(actor=Depends(administrator)):
- return [present(j) | {'user_id':j['user_id']} for j in db.list()[:200]]
+ rows = db.list()[:200]
+ metadata = db.get_presentations([j['id'] for j in rows])
+ return [present(j, metadata.get(j['id'], {})) | {'user_id':j['user_id']} for j in rows]
@app.get('/admin/evaluations/{job_id}/logs')
def admin_logs(job_id:UUID,actor=Depends(administrator)):
@@ -145,15 +147,17 @@ def worker(job_id: UUID, authorization: str | None = Header(default=None)):
if not job: raise HTTPException(404, 'Evaluation not found')
return job
- def present(job):
- job = {**job, 'allocation': db.get_presentation(job['id'], 'allocation')}
- publication=db.get_presentation(job['id'],'publication')
+ def present(job, metadata=None):
+ if metadata is None:
+ metadata = db.get_presentations([job['id']]).get(job['id'], {})
+ job = {**job, 'allocation': metadata.get('allocation')}
+ publication=metadata.get('publication')
if job['state']=='succeeded':
desired=job['config'].get('visibility')=='public'
if desired and not publication:publication={'state':'pending'}
elif publication and publication.get('desired_public')!=desired:
publication={**publication,'state':'pending' if desired else 'unpublishing'}
- summary=db.get_presentation(job['id'],'summary') or {}
+ summary=metadata.get('summary') or {}
return public_job(job) | {'publication':publication, 'omitted_count': summary.get('omitted_count',0)}
@app.get('/health')
@@ -240,11 +244,15 @@ def schema(): return EvaluationRequest.model_json_schema()
@app.get('/evaluations')
def evaluations(user_id=Depends(user)):
- return [present(job) for job in db.list(user_id)]
+ rows = db.list(user_id)
+ metadata = db.get_presentations([j['id'] for j in rows])
+ return [present(job, metadata.get(job['id'], {})) for job in rows]
@app.get('/experiments')
def published():
- return [present(j) for j in db.public_jobs() if db.get_presentation(j['id'], 'summary')]
+ rows = db.public_jobs()
+ metadata = db.get_presentations([j['id'] for j in rows])
+ return [present(j, metadata.get(j['id'], {})) for j in rows if metadata.get(j['id'], {}).get('summary')]
def readable(job_id, authorization):
job = db.get(str(job_id))
diff --git a/backend/app/db.py b/backend/app/db.py
index 552e025..aa3d272 100644
--- a/backend/app/db.py
+++ b/backend/app/db.py
@@ -185,6 +185,18 @@ def get_presentation(self, job_id, name):
with self.engine.connect() as c:
return c.execute(select(presentation.c.data).where(presentation.c.job_id == job_id, presentation.c.name == name)).scalar()
+ def get_presentations(self, job_ids):
+ if not job_ids:
+ return {}
+ with self.engine.connect() as c:
+ rows = c.execute(select(presentation).where(
+ presentation.c.job_id.in_(job_ids),
+ presentation.c.name.in_(('allocation', 'publication', 'summary')))).mappings()
+ result = {}
+ for row in rows:
+ result.setdefault(row['job_id'], {})[row['name']] = row['data']
+ return result
+
def visibility(self, job_id, value):
with self.engine.begin() as c:
row = c.execute(select(jobs).where(jobs.c.id == job_id).with_for_update()).mappings().one()
diff --git a/backend/tests/test_backend.py b/backend/tests/test_backend.py
index 78588a5..4952a91 100644
--- a/backend/tests/test_backend.py
+++ b/backend/tests/test_backend.py
@@ -386,3 +386,28 @@ def test_auto_cpu_respects_cpu_limit(service):
response=submit(client,payload(compute_type='gpu'))
assert response.status_code==403
assert 'max_cpu_count' in response.json()['detail']
+
+
+def test_history_batches_metadata_and_keeps_owner_scope(service):
+ from sqlalchemy import event
+ _, db, client = service
+ first = submit(client).json()['id']
+ second = submit(client, key='two').json()['id']
+ foreign = submit(client, user=OTHER).json()['id']
+ db.put_presentation(first, 'summary', {'omitted_count': 3})
+ db.put_presentation(foreign, 'summary', {'omitted_count': 999})
+ statements = []
+ def capture(conn, cursor, statement, parameters, context, executemany):
+ if statement.lstrip().upper().startswith('SELECT') and 'va_presentation' in statement:
+ statements.append(statement)
+ event.listen(db.engine, 'before_cursor_execute', capture)
+ try:
+ response = client.get('/evaluations', headers={'Authorization': 'Bearer '+USER})
+ finally:
+ event.remove(db.engine, 'before_cursor_execute', capture)
+ assert response.status_code == 200
+ rows = {row['id']: row for row in response.json()}
+ assert set(rows) == {first, second}
+ assert rows[first]['omitted_count'] == 3
+ assert rows[second]['omitted_count'] == 0
+ assert len(statements) == 1
diff --git a/next-src/src/app/pages.css b/next-src/src/app/pages.css
index b4e5025..f502ad0 100644
--- a/next-src/src/app/pages.css
+++ b/next-src/src/app/pages.css
@@ -1015,6 +1015,15 @@ body:has(.arena-home) .page-hills {
.eval-result-links > span { color: var(--text-muted); font-size: 12px; }
@media (max-width: 600px) { .eval-result-links { padding: 0 16px 16px; gap: 8px; } .eval-result-links > span { flex-basis: 100%; } }
+.eval-success-section { margin: 24px 0 36px; }
+.eval-success-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: 16px; }
+.eval-success-table { width: 100%; border-collapse: collapse; text-align: left; font-size: 14px; }
+.eval-success-table th, .eval-success-table td { padding: 16px 20px; border-bottom: 1px solid var(--border); vertical-align: middle; }
+.eval-success-table thead th { color: var(--text-muted); font-size: 12px; font-weight: 600; white-space: nowrap; }
+.eval-success-table tbody th { font-weight: 600; min-width: 150px; overflow-wrap: anywhere; }
+.eval-success-table tbody tr:last-child > * { border-bottom: 0; }
+.eval-success-actions { display: flex; gap: 14px; flex-wrap: wrap; }
+.eval-success-actions a { white-space: nowrap; text-underline-offset: 4px; }
/* ---------- Evaluate: generation options ---------- */
.eval-generation {
margin: 8px 0 18px;
diff --git a/next-src/src/components/EvaluationRunner.tsx b/next-src/src/components/EvaluationRunner.tsx
index 5e29232..8dc96af 100644
--- a/next-src/src/components/EvaluationRunner.tsx
+++ b/next-src/src/components/EvaluationRunner.tsx
@@ -388,7 +388,7 @@ export function EvaluationRunner() {
{busy ? 'Preparing…' : 'Run evaluation →'} {blockers.length>0 &&
Before you can run {blockers.map((message,i)=>{message} )} }{error &&
{error}
}
Runs continue in the background until complete or cancelled.{limits.max_runtime_seconds!==null && ` Admin runtime limit: ${Math.round(limits.max_runtime_seconds/60)} minutes.`}{!ownKeys && limits.require_credits && " Runs also stop when their reserved execution credits are used; unused time is returned."}
}
- {tab === 'runs' && Your evaluations {!runsLoaded && !jobs.length && !runsError && Loading your evaluations…
}{runsError && {runsError}
setRunsRetry(n => n + 1)}>{runsRefreshing ? 'Retrying…' : 'Retry loading evaluations'} }{runsLoaded && !jobs.length && !runsError && No evaluations yet. Submitted runs will appear here.
}{jobs.map(job => { const open = openRuns[job.id] ?? isActive(job.state); const body = `run-body-${job.id}`; return
+ {tab === 'runs' && Your evaluations {!runsLoaded && !jobs.length && !runsError && Loading your evaluations…
}{runsError && {runsError}
setRunsRetry(n => n + 1)}>{runsRefreshing ? 'Retrying…' : 'Retry loading evaluations'} }{runsLoaded && !jobs.length && !runsError && No evaluations yet. Submitted runs will appear here.
}{jobs.some(job => job.state === 'succeeded') && Successful runs Run Constitution Models Scenarios Results {jobs.filter(job => job.state === 'succeeded').map(job => {job.name} {job.constitution} {job.models_count} {job.scenario_count} )}
}{jobs.length > 0 && All runs }{jobs.map(job => { const open = openRuns[job.id] ?? isActive(job.state); const body = `run-body-${job.id}`; return
{/* Active runs start open; finished ones stay collapsed until clicked */}
setOpenRuns(o => ({ ...o, [job.id]: !open }))}>{job.state === 'succeeded' ? 'Completed' : label(job.state)} {job.name} {job.constitution} · {job.models_count} models · {job.scenario_count} scenarios · {job.visibility} · {job.engine === 'inspect' ? 'Inspect' : 'Native'}
{job.state === 'succeeded' && }