Spaces:
Running
Make the manage panel work across versions, and fix what it did wrong
Browse filesThree defects, all from the panel predating versioning.
Delete removed the queue entry as well as the score. The request is shared by every
version, so deleting a v8 score also destroyed the v7 row's metadata and left no way
to re-run without resubmitting. Delete is now two actions: 'Delete <version> score'
drops that version's file and keeps the queue entry, and 'Remove entirely' takes the
request plus every version's score. The old wording promised the second while the
button was reached from a single version.
Nothing showed which versions a model was actually on. The status badge is global --
one request serves all versions -- so a model reading FINISHED could have no score on
the board being viewed. Each row now carries a chip per version, and the panel header
counts how many are scored versus missing on the current one.
There was no way to start a version. The worker only claims PENDING / RUNNING / RERUN,
so after a sweep every model sits at FINISHED and a new version would evaluate nothing
at all. /api/requeue-all sets them back to RERUN, defaulting to only the models with no
score on the target version so a half-finished sweep resumes instead of restarting.
test_manage.py covers all of it against a stubbed Hub: per-version presence, that a
scoped delete leaves the other version and the request alone, that scope=all removes
both, and that requeue skips models already scored or already queued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- app.py +101 -20
- index.html +70 -9
- test_manage.py +95 -0
|
@@ -286,11 +286,16 @@ def _results_paths(model: str, version: str | None = None) -> list[str]:
|
|
| 286 |
class ModelRef(BaseModel):
|
| 287 |
model: str
|
| 288 |
version: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
|
| 291 |
@app.get("/api/manage")
|
| 292 |
def manage(request: Request, version: str | None = None):
|
| 293 |
-
"""Every request +
|
| 294 |
_require_owner(request, check_origin=False) # read-only GET
|
| 295 |
api = HfApi(token=HF_TOKEN)
|
| 296 |
have = {}
|
|
@@ -304,10 +309,16 @@ def manage(request: Request, version: str | None = None):
|
|
| 304 |
rows = []
|
| 305 |
for r in _list_requests():
|
| 306 |
m = r.get("model", "")
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
rows.append({
|
| 312 |
"model": m,
|
| 313 |
"status": r.get("status"),
|
|
@@ -315,10 +326,12 @@ def manage(request: Request, version: str | None = None):
|
|
| 315 |
"submitted_by": r.get("submitted_by"),
|
| 316 |
"source": r.get("source", "open"),
|
| 317 |
"visibility": r.get("visibility", "public"),
|
| 318 |
-
"result_in":
|
|
|
|
| 319 |
})
|
| 320 |
rows.sort(key=lambda x: x.get("submitted_time") or "", reverse=True)
|
| 321 |
-
return {"models": rows, "owner": LEADERBOARD_OWNER
|
|
|
|
| 322 |
|
| 323 |
|
| 324 |
@app.post("/api/model/delete")
|
|
@@ -329,19 +342,25 @@ def model_delete(request: Request, body: ModelRef):
|
|
| 329 |
if not MODEL_RE.match(model):
|
| 330 |
raise HTTPException(400, "Bad model id.")
|
| 331 |
api = HfApi(token=HF_TOKEN)
|
|
|
|
|
|
|
|
|
|
| 332 |
removed = []
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
| 342 |
version = _clean_version(body.version)
|
|
|
|
|
|
|
| 343 |
for repo in (RESULTS_REPO, RESULTS_PRIVATE_REPO):
|
| 344 |
-
for path in
|
| 345 |
try:
|
| 346 |
api.delete_file(path_in_repo=path, repo_id=repo,
|
| 347 |
repo_type="dataset",
|
|
@@ -350,9 +369,13 @@ def model_delete(request: Request, body: ModelRef):
|
|
| 350 |
except Exception:
|
| 351 |
pass # simply not present there
|
| 352 |
if not removed:
|
| 353 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
return {"ok": True, "removed": removed,
|
| 355 |
-
"message": f"Deleted {
|
| 356 |
|
| 357 |
|
| 358 |
@app.post("/api/model/rerun")
|
|
@@ -377,7 +400,65 @@ def model_rerun(request: Request, body: ModelRef):
|
|
| 377 |
path_or_fileobj=json.dumps(payload, ensure_ascii=False, indent=1).encode("utf-8"),
|
| 378 |
path_in_repo=hit["_path"], repo_id=REQUESTS_REPO, repo_type="dataset",
|
| 379 |
commit_message=f"{model} -> RERUN")
|
| 380 |
-
return {"ok": True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
|
| 383 |
@app.get("/api/private-results")
|
|
|
|
| 286 |
class ModelRef(BaseModel):
|
| 287 |
model: str
|
| 288 |
version: str | None = None
|
| 289 |
+
# "version" removes only this version's score and leaves the queue entry, so the
|
| 290 |
+
# model can be re-evaluated. "all" removes the queue entry and every version's
|
| 291 |
+
# score. Deleting a v8 score used to destroy the shared request too, which took
|
| 292 |
+
# the v7 row's metadata with it and left no way to re-run without resubmitting.
|
| 293 |
+
scope: str = "version"
|
| 294 |
|
| 295 |
|
| 296 |
@app.get("/api/manage")
|
| 297 |
def manage(request: Request, version: str | None = None):
|
| 298 |
+
"""Every request + where its scores live, per benchmark version. Owner-only."""
|
| 299 |
_require_owner(request, check_origin=False) # read-only GET
|
| 300 |
api = HfApi(token=HF_TOKEN)
|
| 301 |
have = {}
|
|
|
|
| 309 |
rows = []
|
| 310 |
for r in _list_requests():
|
| 311 |
m = r.get("model", "")
|
| 312 |
+
# Presence per version, so the panel can show at a glance which boards a
|
| 313 |
+
# model is actually on. `result_in` stays for the selected version because
|
| 314 |
+
# the older UI read it.
|
| 315 |
+
by_version = {}
|
| 316 |
+
for v in VERSIONS:
|
| 317 |
+
where = set()
|
| 318 |
+
for path in _results_paths(m, v):
|
| 319 |
+
where |= have.get(path, set())
|
| 320 |
+
if where:
|
| 321 |
+
by_version[v] = sorted(where)
|
| 322 |
rows.append({
|
| 323 |
"model": m,
|
| 324 |
"status": r.get("status"),
|
|
|
|
| 326 |
"submitted_by": r.get("submitted_by"),
|
| 327 |
"source": r.get("source", "open"),
|
| 328 |
"visibility": r.get("visibility", "public"),
|
| 329 |
+
"result_in": by_version.get(_clean_version(version), []),
|
| 330 |
+
"results_by_version": by_version,
|
| 331 |
})
|
| 332 |
rows.sort(key=lambda x: x.get("submitted_time") or "", reverse=True)
|
| 333 |
+
return {"models": rows, "owner": LEADERBOARD_OWNER,
|
| 334 |
+
"versions": VERSIONS, "version": _clean_version(version)}
|
| 335 |
|
| 336 |
|
| 337 |
@app.post("/api/model/delete")
|
|
|
|
| 342 |
if not MODEL_RE.match(model):
|
| 343 |
raise HTTPException(400, "Bad model id.")
|
| 344 |
api = HfApi(token=HF_TOKEN)
|
| 345 |
+
scope = (body.scope or "version").strip().lower()
|
| 346 |
+
if scope not in ("version", "all"):
|
| 347 |
+
raise HTTPException(400, "scope must be 'version' or 'all'.")
|
| 348 |
removed = []
|
| 349 |
+
if scope == "all":
|
| 350 |
+
for r in _list_requests():
|
| 351 |
+
if r.get("model") == model and r.get("_path"):
|
| 352 |
+
try:
|
| 353 |
+
api.delete_file(path_in_repo=r["_path"], repo_id=REQUESTS_REPO,
|
| 354 |
+
repo_type="dataset",
|
| 355 |
+
commit_message=f"Remove request {model}")
|
| 356 |
+
removed.append(f"request:{r['_path']}")
|
| 357 |
+
except Exception as e:
|
| 358 |
+
print(f"[delete] request {model}: {type(e).__name__}: {e}")
|
| 359 |
version = _clean_version(body.version)
|
| 360 |
+
targets = ([p for v in VERSIONS for p in _results_paths(model, v)]
|
| 361 |
+
if scope == "all" else _results_paths(model, version))
|
| 362 |
for repo in (RESULTS_REPO, RESULTS_PRIVATE_REPO):
|
| 363 |
+
for path in dict.fromkeys(targets):
|
| 364 |
try:
|
| 365 |
api.delete_file(path_in_repo=path, repo_id=repo,
|
| 366 |
repo_type="dataset",
|
|
|
|
| 369 |
except Exception:
|
| 370 |
pass # simply not present there
|
| 371 |
if not removed:
|
| 372 |
+
raise HTTPException(
|
| 373 |
+
404,
|
| 374 |
+
f"{model} has no score on {version}." if scope == "version"
|
| 375 |
+
else f"Nothing found for {model}.")
|
| 376 |
+
what = f"every version of {model}" if scope == "all" else f"{model} from {version}"
|
| 377 |
return {"ok": True, "removed": removed,
|
| 378 |
+
"message": f"Deleted {what} ({len(removed)} file(s))."}
|
| 379 |
|
| 380 |
|
| 381 |
@app.post("/api/model/rerun")
|
|
|
|
| 400 |
path_or_fileobj=json.dumps(payload, ensure_ascii=False, indent=1).encode("utf-8"),
|
| 401 |
path_in_repo=hit["_path"], repo_id=REQUESTS_REPO, repo_type="dataset",
|
| 402 |
commit_message=f"{model} -> RERUN")
|
| 403 |
+
return {"ok": True,
|
| 404 |
+
"message": f"{model} queued for re-evaluation (RERUN). It lands on "
|
| 405 |
+
f"whichever version the worker is running (its RUN_ID)."}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
class RequeueRef(BaseModel):
|
| 409 |
+
version: str | None = None
|
| 410 |
+
# Default to only what is missing, so re-running a half-finished sweep does not
|
| 411 |
+
# discard the models that already landed.
|
| 412 |
+
only_missing: bool = True
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
@app.post("/api/requeue-all")
|
| 416 |
+
def requeue_all(request: Request, body: RequeueRef):
|
| 417 |
+
"""Set every queued model to RERUN so a whole version can be evaluated.
|
| 418 |
+
|
| 419 |
+
The worker only ever picks up PENDING / RUNNING / RERUN, so after a sweep every
|
| 420 |
+
model sits at FINISHED and a new version would evaluate nothing at all. This is
|
| 421 |
+
the switch that starts the next version's run."""
|
| 422 |
+
_require_owner(request)
|
| 423 |
+
version = _clean_version(body.version)
|
| 424 |
+
api = HfApi(token=HF_TOKEN)
|
| 425 |
+
|
| 426 |
+
have = set()
|
| 427 |
+
for repo in (RESULTS_REPO, RESULTS_PRIVATE_REPO):
|
| 428 |
+
try:
|
| 429 |
+
have |= {f for f in api.list_repo_files(repo, repo_type="dataset")
|
| 430 |
+
if f.endswith(".json")}
|
| 431 |
+
except Exception:
|
| 432 |
+
pass
|
| 433 |
+
|
| 434 |
+
queued, skipped = [], []
|
| 435 |
+
for r in _list_requests():
|
| 436 |
+
model = r.get("model")
|
| 437 |
+
if not model or not r.get("_path"):
|
| 438 |
+
continue
|
| 439 |
+
scored = any(p in have for p in _results_paths(model, version))
|
| 440 |
+
if body.only_missing and scored:
|
| 441 |
+
skipped.append(model)
|
| 442 |
+
continue
|
| 443 |
+
if str(r.get("status") or "").upper() in ("PENDING", "RUNNING", "RERUN"):
|
| 444 |
+
skipped.append(model) # already on the worker's list
|
| 445 |
+
continue
|
| 446 |
+
payload = {k: v for k, v in r.items() if k != "_path"}
|
| 447 |
+
payload["status"] = "RERUN"
|
| 448 |
+
try:
|
| 449 |
+
api.upload_file(
|
| 450 |
+
path_or_fileobj=json.dumps(payload, ensure_ascii=False, indent=1).encode("utf-8"),
|
| 451 |
+
path_in_repo=r["_path"], repo_id=REQUESTS_REPO, repo_type="dataset",
|
| 452 |
+
commit_message=f"{model} -> RERUN (requeue for {version})")
|
| 453 |
+
queued.append(model)
|
| 454 |
+
except Exception as e:
|
| 455 |
+
print(f"[requeue-all] {model}: {type(e).__name__}: {e}")
|
| 456 |
+
msg = (f"Queued {len(queued)} model(s) for {version}."
|
| 457 |
+
+ (f" Skipped {len(skipped)} already scored or already queued." if skipped else ""))
|
| 458 |
+
if not queued:
|
| 459 |
+
msg = (f"Nothing to queue for {version} — "
|
| 460 |
+
f"{len(skipped)} model(s) are already scored or already in the queue.")
|
| 461 |
+
return {"ok": True, "queued": queued, "skipped": skipped, "message": msg}
|
| 462 |
|
| 463 |
|
| 464 |
@app.get("/api/private-results")
|
|
@@ -172,6 +172,13 @@ table.board tbody tr:hover td:nth-child(1),table.board tbody tr:hover td:nth-chi
|
|
| 172 |
.mbtn:hover{border-color:var(--accent);color:var(--accent)}
|
| 173 |
.mbtn.danger:hover{border-color:#e06666;color:#e06666}
|
| 174 |
.b-private{background:rgba(120,120,200,.16);color:#9aa4e0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
.prog{margin:2px 0 12px;padding:11px 13px;border-radius:12px;background:rgba(var(--cell-heat),.07);
|
| 176 |
border:1px solid var(--border)}
|
| 177 |
.prog .ptop{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;font-size:13px}
|
|
@@ -293,6 +300,12 @@ footer{margin-top:30px;text-align:center;color:var(--ink-mute);font-size:12px}
|
|
| 293 |
<div class="grid2 one" id="managePanel" hidden>
|
| 294 |
<div class="panel">
|
| 295 |
<h3><span class="k">Manage</span> Your submitted models<span id="manageCount" class="qcount"></span></h3>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
<div id="manageList"><div class="mrow" style="color:var(--ink-mute)">Loading…</div></div>
|
| 297 |
<div class="msg" id="manageMsg"></div>
|
| 298 |
</div>
|
|
@@ -923,7 +936,13 @@ async function loadManage(){
|
|
| 923 |
const d=await r.json();
|
| 924 |
panel.hidden=false;
|
| 925 |
const models=Array.isArray(d.models)?d.models:[];
|
|
|
|
| 926 |
document.getElementById('manageCount').textContent=`${models.length} submitted`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
if(!models.length){list.innerHTML='<div class="mrow" style="color:var(--ink-mute)">Nothing submitted yet.</div>';return;}
|
| 928 |
list.innerHTML=models.map(m=>{
|
| 929 |
const s=(m.status||'').toUpperCase();
|
|
@@ -931,13 +950,30 @@ async function loadManage(){
|
|
| 931 |
const tags=[`<span class="badge ${cls}">${s}</span>`];
|
| 932 |
if(m.visibility==='private')tags.push('<span class="badge b-private">PRIVATE</span>');
|
| 933 |
if(m.source==='closed')tags.push('<span class="badge b-pending">API</span>');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 934 |
return `<div class="mrow" data-model="${esc(m.model)}">
|
| 935 |
<span class="mname" title="${esc(m.model)}">${esc(m.model)}</span>
|
| 936 |
-
${tags.join('')}
|
| 937 |
<button class="mbtn" data-act="rerun">Re-run</button>
|
| 938 |
-
<button class="mbtn danger" data-act="delete">Delete</button>
|
|
|
|
| 939 |
</div>`;}).join('');
|
| 940 |
list.querySelectorAll('button[data-act]').forEach(b=>b.onclick=()=>manageAct(b));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
}catch(e){
|
| 942 |
panel.hidden=true;
|
| 943 |
list.innerHTML=`<div class="mrow" style="color:var(--ink-mute)">Could not load (${esc(String(e))}).</div>`;
|
|
@@ -946,21 +982,46 @@ async function loadManage(){
|
|
| 946 |
|
| 947 |
async function manageAct(btn){
|
| 948 |
const row=btn.closest('.mrow'), model=row.dataset.model, act=btn.dataset.act;
|
| 949 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
row.querySelectorAll('button').forEach(b=>b.disabled=true);
|
| 951 |
-
btn.textContent=act==='
|
|
|
|
|
|
|
|
|
|
| 952 |
try{
|
| 953 |
-
const r=await fetch(
|
| 954 |
-
headers:{'Content-Type':'application/json'},body:JSON.stringify(
|
| 955 |
const d=await r.json().catch(()=>({}));
|
| 956 |
if(r.ok){mmsg(d.message||'Done.','ok');await loadManage();
|
| 957 |
-
if(act==
|
| 958 |
else{mmsg(d.detail||d.message||('Error '+r.status),'err');
|
| 959 |
row.querySelectorAll('button').forEach(b=>b.disabled=false);
|
| 960 |
-
btn.textContent=
|
| 961 |
}catch(e){mmsg('Network error — please try again.','err');
|
| 962 |
row.querySelectorAll('button').forEach(b=>b.disabled=false);
|
| 963 |
-
btn.textContent=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
}
|
| 965 |
|
| 966 |
// show the pinned-column edge shadow only while the table is scrolled right
|
|
|
|
| 172 |
.mbtn:hover{border-color:var(--accent);color:var(--accent)}
|
| 173 |
.mbtn.danger:hover{border-color:#e06666;color:#e06666}
|
| 174 |
.b-private{background:rgba(120,120,200,.16);color:#9aa4e0}
|
| 175 |
+
.vchip{font:600 10px "IBM Plex Mono";padding:2px 7px;border-radius:6px;border:1px solid var(--border);
|
| 176 |
+
color:var(--ink-mute);white-space:nowrap}
|
| 177 |
+
.vchip.on{border-color:var(--accent);color:var(--accent)}
|
| 178 |
+
.vchip.here{box-shadow:inset 0 0 0 1px var(--accent)}
|
| 179 |
+
.mbar{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:9px 0 11px;
|
| 180 |
+
border-bottom:1px solid var(--border);margin-bottom:6px}
|
| 181 |
+
.mbar .grow{flex:1 1 auto}
|
| 182 |
.prog{margin:2px 0 12px;padding:11px 13px;border-radius:12px;background:rgba(var(--cell-heat),.07);
|
| 183 |
border:1px solid var(--border)}
|
| 184 |
.prog .ptop{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;font-size:13px}
|
|
|
|
| 300 |
<div class="grid2 one" id="managePanel" hidden>
|
| 301 |
<div class="panel">
|
| 302 |
<h3><span class="k">Manage</span> Your submitted models<span id="manageCount" class="qcount"></span></h3>
|
| 303 |
+
<div class="mbar" id="manageBar" hidden>
|
| 304 |
+
<span class="vchip here" id="mbarVer"></span>
|
| 305 |
+
<span class="grow" style="font-size:12px;color:var(--ink-mute)" id="mbarNote"></span>
|
| 306 |
+
<button class="mbtn" id="btnQueueMissing">Queue models missing here</button>
|
| 307 |
+
<button class="mbtn" id="btnQueueAll">Queue every model</button>
|
| 308 |
+
</div>
|
| 309 |
<div id="manageList"><div class="mrow" style="color:var(--ink-mute)">Loading…</div></div>
|
| 310 |
<div class="msg" id="manageMsg"></div>
|
| 311 |
</div>
|
|
|
|
| 936 |
const d=await r.json();
|
| 937 |
panel.hidden=false;
|
| 938 |
const models=Array.isArray(d.models)?d.models:[];
|
| 939 |
+
const versions=Array.isArray(d.versions)&&d.versions.length?d.versions:VERSIONS.map(v=>v.id);
|
| 940 |
document.getElementById('manageCount').textContent=`${models.length} submitted`;
|
| 941 |
+
const missing=models.length-models.filter(m=>((m.results_by_version||{})[CURRENT.id]||[]).length).length;
|
| 942 |
+
document.getElementById('manageBar').hidden=false;
|
| 943 |
+
document.getElementById('mbarVer').textContent='managing '+CURRENT.id;
|
| 944 |
+
document.getElementById('mbarNote').textContent=
|
| 945 |
+
`${models.length-missing} scored on ${CURRENT.id}, ${missing} missing`;
|
| 946 |
if(!models.length){list.innerHTML='<div class="mrow" style="color:var(--ink-mute)">Nothing submitted yet.</div>';return;}
|
| 947 |
list.innerHTML=models.map(m=>{
|
| 948 |
const s=(m.status||'').toUpperCase();
|
|
|
|
| 950 |
const tags=[`<span class="badge ${cls}">${s}</span>`];
|
| 951 |
if(m.visibility==='private')tags.push('<span class="badge b-private">PRIVATE</span>');
|
| 952 |
if(m.source==='closed')tags.push('<span class="badge b-pending">API</span>');
|
| 953 |
+
// Where this model actually has a score. The status badge above is global —
|
| 954 |
+
// one request serves every version — so it says nothing about which board the
|
| 955 |
+
// model is on. These chips do.
|
| 956 |
+
const byv=m.results_by_version||{};
|
| 957 |
+
const chips=versions.map(v=>{
|
| 958 |
+
const on=Array.isArray(byv[v])&&byv[v].length;
|
| 959 |
+
const priv=on&&byv[v].includes('private')&&!byv[v].includes('public');
|
| 960 |
+
return `<span class="vchip${on?' on':''}${v===CURRENT.id?' here':''}" title="${
|
| 961 |
+
on?`scored on ${v}${priv?' (private)':''}`:`no score on ${v}`}">${v}${on?(priv?' ●':' ✓'):' —'}</span>`;
|
| 962 |
+
}).join('');
|
| 963 |
+
const hasHere=Array.isArray(byv[CURRENT.id])&&byv[CURRENT.id].length;
|
| 964 |
return `<div class="mrow" data-model="${esc(m.model)}">
|
| 965 |
<span class="mname" title="${esc(m.model)}">${esc(m.model)}</span>
|
| 966 |
+
${tags.join('')}${chips}
|
| 967 |
<button class="mbtn" data-act="rerun">Re-run</button>
|
| 968 |
+
<button class="mbtn danger" data-act="delete"${hasHere?'':' disabled title="no score on this version"'}>Delete ${esc(CURRENT.id)} score</button>
|
| 969 |
+
<button class="mbtn danger" data-act="purge">Remove entirely</button>
|
| 970 |
</div>`;}).join('');
|
| 971 |
list.querySelectorAll('button[data-act]').forEach(b=>b.onclick=()=>manageAct(b));
|
| 972 |
+
const bm=document.getElementById('btnQueueMissing'), ba=document.getElementById('btnQueueAll');
|
| 973 |
+
bm.textContent=`Queue ${missing} missing on ${CURRENT.id}`;
|
| 974 |
+
bm.disabled=!missing;
|
| 975 |
+
bm.onclick=()=>requeue(true,bm);
|
| 976 |
+
ba.onclick=()=>requeue(false,ba);
|
| 977 |
}catch(e){
|
| 978 |
panel.hidden=true;
|
| 979 |
list.innerHTML=`<div class="mrow" style="color:var(--ink-mute)">Could not load (${esc(String(e))}).</div>`;
|
|
|
|
| 982 |
|
| 983 |
async function manageAct(btn){
|
| 984 |
const row=btn.closest('.mrow'), model=row.dataset.model, act=btn.dataset.act;
|
| 985 |
+
const label=btn.textContent;
|
| 986 |
+
// 'delete' drops only the score on the version being viewed. 'purge' removes the
|
| 987 |
+
// queue entry and every version's score — the request is shared across versions,
|
| 988 |
+
// so purging while looking at v8 also takes the v7 row down.
|
| 989 |
+
if(act==='delete'&&!confirm(
|
| 990 |
+
`Remove ${model}'s ${CURRENT.id} score?\n\nIts queue entry stays, so it can be evaluated again. Scores on other versions are untouched.`))return;
|
| 991 |
+
if(act==='purge'&&!confirm(
|
| 992 |
+
`Remove ${model} entirely?\n\nThis deletes its queue entry AND its score on EVERY version, ${CURRENT.id} included. It cannot be undone — only a fresh submission would bring it back.`))return;
|
| 993 |
row.querySelectorAll('button').forEach(b=>b.disabled=true);
|
| 994 |
+
btn.textContent=act==='rerun'?'Queuing…':'Removing…';
|
| 995 |
+
const url=act==='rerun'?'/api/model/rerun':'/api/model/delete';
|
| 996 |
+
const body={model,version:CURRENT.id};
|
| 997 |
+
if(act!=='rerun')body.scope=(act==='purge')?'all':'version';
|
| 998 |
try{
|
| 999 |
+
const r=await fetch(url,{method:'POST',credentials:'same-origin',
|
| 1000 |
+
headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
| 1001 |
const d=await r.json().catch(()=>({}));
|
| 1002 |
if(r.ok){mmsg(d.message||'Done.','ok');await loadManage();
|
| 1003 |
+
if(act!=='rerun')setTimeout(()=>location.reload(),1200);}
|
| 1004 |
else{mmsg(d.detail||d.message||('Error '+r.status),'err');
|
| 1005 |
row.querySelectorAll('button').forEach(b=>b.disabled=false);
|
| 1006 |
+
btn.textContent=label;}
|
| 1007 |
}catch(e){mmsg('Network error — please try again.','err');
|
| 1008 |
row.querySelectorAll('button').forEach(b=>b.disabled=false);
|
| 1009 |
+
btn.textContent=label;}
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
async function requeue(onlyMissing,btn){
|
| 1013 |
+
const what=onlyMissing?`every model with no ${CURRENT.id} score`:`ALL models, including the ${CURRENT.id} ones already scored`;
|
| 1014 |
+
if(!confirm(`Queue ${what} for re-evaluation?\n\nThey run on whichever version the worker has as its RUN_ID — make sure that is ${CURRENT.id} before starting it.`))return;
|
| 1015 |
+
const label=btn.textContent; btn.disabled=true; btn.textContent='Queuing…';
|
| 1016 |
+
try{
|
| 1017 |
+
const r=await fetch('/api/requeue-all',{method:'POST',credentials:'same-origin',
|
| 1018 |
+
headers:{'Content-Type':'application/json'},
|
| 1019 |
+
body:JSON.stringify({version:CURRENT.id,only_missing:onlyMissing})});
|
| 1020 |
+
const d=await r.json().catch(()=>({}));
|
| 1021 |
+
mmsg(r.ok?(d.message||'Queued.'):(d.detail||d.message||('Error '+r.status)),r.ok?'ok':'err');
|
| 1022 |
+
if(r.ok)await loadManage();
|
| 1023 |
+
}catch(e){mmsg('Network error — please try again.','err');}
|
| 1024 |
+
btn.disabled=false; btn.textContent=label;
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
// show the pinned-column edge shadow only while the table is scrolled right
|
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exercise the real manage/delete/requeue routes with the Hub stubbed out."""
|
| 2 |
+
import sys, types, json, importlib.util
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
os.environ["HF_TOKEN"] = "stub-token"
|
| 6 |
+
os.environ["LEADERBOARD_OWNER"] = "Mushari440"
|
| 7 |
+
|
| 8 |
+
# ---- fake Hub state -------------------------------------------------------
|
| 9 |
+
FILES = {
|
| 10 |
+
"Mushari440/results": {
|
| 11 |
+
"v7/org/results_alpha.json", "v7/org/results_beta.json",
|
| 12 |
+
"v8/org/results_alpha.json",
|
| 13 |
+
},
|
| 14 |
+
"Mushari440/results-private": set(),
|
| 15 |
+
"Mushari440/requests": {"org/alpha.json", "org/beta.json", "org/gamma.json"},
|
| 16 |
+
}
|
| 17 |
+
REQ = {
|
| 18 |
+
"org/alpha.json": {"model": "org/alpha", "status": "FINISHED"},
|
| 19 |
+
"org/beta.json": {"model": "org/beta", "status": "FINISHED"},
|
| 20 |
+
"org/gamma.json": {"model": "org/gamma", "status": "PENDING"},
|
| 21 |
+
}
|
| 22 |
+
COMMITS = []
|
| 23 |
+
|
| 24 |
+
class FakeApi:
|
| 25 |
+
def __init__(self, *a, **k): pass
|
| 26 |
+
def list_repo_files(self, repo_id=None, repo_type=None, *a, **k):
|
| 27 |
+
return sorted(FILES.get(repo_id or a[0] if a else repo_id, set()))
|
| 28 |
+
def delete_file(self, path_in_repo=None, repo_id=None, **k):
|
| 29 |
+
if path_in_repo not in FILES.get(repo_id, set()):
|
| 30 |
+
raise RuntimeError("404")
|
| 31 |
+
FILES[repo_id].discard(path_in_repo)
|
| 32 |
+
if repo_id == "Mushari440/requests":
|
| 33 |
+
REQ.pop(path_in_repo, None) # the queue entry lives in both fakes
|
| 34 |
+
COMMITS.append(("delete", repo_id, path_in_repo))
|
| 35 |
+
def upload_file(self, path_or_fileobj=None, path_in_repo=None, repo_id=None, **k):
|
| 36 |
+
body = json.loads(path_or_fileobj.decode() if isinstance(path_or_fileobj, bytes) else path_or_fileobj)
|
| 37 |
+
REQ[path_in_repo] = body
|
| 38 |
+
COMMITS.append(("upload", repo_id, path_in_repo, body.get("status")))
|
| 39 |
+
|
| 40 |
+
spec = importlib.util.spec_from_file_location("spaceapp", os.path.join(os.path.dirname(__file__), "app.py"))
|
| 41 |
+
app = importlib.util.module_from_spec(spec)
|
| 42 |
+
sys.modules["spaceapp"] = app
|
| 43 |
+
spec.loader.exec_module(app)
|
| 44 |
+
|
| 45 |
+
app.HfApi = FakeApi
|
| 46 |
+
app.api = FakeApi()
|
| 47 |
+
app._require_owner = lambda *a, **k: "Mushari440"
|
| 48 |
+
app._list_requests = lambda: [dict(v, _path=k) for k, v in REQ.items()]
|
| 49 |
+
|
| 50 |
+
fails = []
|
| 51 |
+
def check(name, got, want):
|
| 52 |
+
ok = got == want
|
| 53 |
+
if not ok:
|
| 54 |
+
fails.append(name)
|
| 55 |
+
print(f" FAIL {name}\n got {got}\n want {want}")
|
| 56 |
+
else:
|
| 57 |
+
print(f" PASS {name}")
|
| 58 |
+
|
| 59 |
+
R = types.SimpleNamespace(headers={}, cookies={}, session={})
|
| 60 |
+
|
| 61 |
+
print("manage reports presence per version")
|
| 62 |
+
rows = {m["model"]: m for m in app.manage(R, version="v8")["models"]}
|
| 63 |
+
check("alpha scored on v7 and v8", sorted(rows["alpha" if "alpha" in rows else "org/alpha"]["results_by_version"]), ["v7", "v8"])
|
| 64 |
+
check("beta scored on v7 only", sorted(rows["org/beta"]["results_by_version"]), ["v7"])
|
| 65 |
+
check("gamma scored nowhere", rows["org/gamma"]["results_by_version"], {})
|
| 66 |
+
check("result_in follows the asked version", rows["org/beta"]["result_in"], [])
|
| 67 |
+
|
| 68 |
+
print("\ndelete scope='version' touches only that version")
|
| 69 |
+
app.model_delete(R, app.ModelRef(model="org/alpha", version="v8", scope="version"))
|
| 70 |
+
check("v8 score gone", "v8/org/results_alpha.json" in FILES["Mushari440/results"], False)
|
| 71 |
+
check("v7 score untouched", "v7/org/results_alpha.json" in FILES["Mushari440/results"], True)
|
| 72 |
+
check("request kept", "org/alpha.json" in REQ, True)
|
| 73 |
+
|
| 74 |
+
print("\ndelete scope='all' removes the model everywhere")
|
| 75 |
+
app.model_delete(R, app.ModelRef(model="org/beta", version="v7", scope="all"))
|
| 76 |
+
check("v7 score gone", "v7/org/results_beta.json" in FILES["Mushari440/results"], False)
|
| 77 |
+
check("request gone", "org/beta.json" in REQ, False)
|
| 78 |
+
|
| 79 |
+
print("\nrequeue only_missing skips models already scored on the target version")
|
| 80 |
+
FILES["Mushari440/results"] = {"v7/org/results_alpha.json", "v8/org/results_alpha.json"}
|
| 81 |
+
REQ["org/alpha.json"] = {"model": "org/alpha", "status": "FINISHED"}
|
| 82 |
+
REQ["org/delta.json"] = {"model": "org/delta", "status": "FINISHED"}
|
| 83 |
+
out = app.requeue_all(R, app.RequeueRef(version="v8", only_missing=True))
|
| 84 |
+
check("alpha skipped (already on v8)", "org/alpha" in out["skipped"], True)
|
| 85 |
+
check("delta queued", "org/delta" in out["queued"], True)
|
| 86 |
+
check("gamma skipped (already PENDING)", "org/gamma" in out["skipped"], True)
|
| 87 |
+
check("delta status is RERUN", REQ["org/delta.json"]["status"], "RERUN")
|
| 88 |
+
|
| 89 |
+
print("\nrequeue only_missing=False re-queues scored models too")
|
| 90 |
+
REQ["org/alpha.json"] = {"model": "org/alpha", "status": "FINISHED"}
|
| 91 |
+
out = app.requeue_all(R, app.RequeueRef(version="v8", only_missing=False))
|
| 92 |
+
check("alpha queued this time", "org/alpha" in out["queued"], True)
|
| 93 |
+
|
| 94 |
+
print("\nFAILURES:", fails or "none")
|
| 95 |
+
sys.exit(1 if fails else 0)
|