webui: add rebuild-from-scratch and delete actions
This commit is contained in:
@@ -758,6 +758,7 @@ async function refreshJobs() {
|
||||
renderJobs(jobs);
|
||||
renderNowBar(jobs);
|
||||
if (selectedId) refreshDetail();
|
||||
else clearDetail();
|
||||
schedulePoll(jobs);
|
||||
}
|
||||
|
||||
@@ -874,6 +875,21 @@ function renderOutputPath(job) {
|
||||
outputPathDiv.replaceChildren(wrap);
|
||||
}
|
||||
|
||||
function clearDetail() {
|
||||
reviewTitle.replaceChildren();
|
||||
reviewError.hidden = true;
|
||||
reviewError.textContent = "";
|
||||
document.getElementById("warnings").replaceChildren(
|
||||
emptyNote("no songs queued yet — nothing to review"));
|
||||
outputPathDiv.replaceChildren();
|
||||
reviewActions.replaceChildren();
|
||||
delete reviewActions.dataset.jobId;
|
||||
reviewLineage.replaceChildren();
|
||||
reviewLineage.hidden = true;
|
||||
delete reviewLineage.dataset.jobId;
|
||||
detail.textContent = "no songs queued yet — nothing to log";
|
||||
}
|
||||
|
||||
function setFormMode(inp) {
|
||||
formHeading.textContent = rebuildOf ? "REBUILD" : "NEW SONG";
|
||||
submitButton.textContent = rebuildOf ? "REBUILD SONG" : "CREATE SONG";
|
||||
@@ -920,21 +936,60 @@ async function rerunJob(id) {
|
||||
refreshJobs();
|
||||
}
|
||||
|
||||
async function rebuildFullJob(id) {
|
||||
formError.textContent = "";
|
||||
const resp = await fetch(`/api/jobs/${id}/rebuild-full`, { method: "POST" });
|
||||
if (!resp.ok) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
formError.textContent = body.detail || `error: ${resp.status}`;
|
||||
return;
|
||||
}
|
||||
selectedId = (await resp.json()).id;
|
||||
refreshJobs();
|
||||
}
|
||||
|
||||
async function deleteJob(id) {
|
||||
if (!confirm("Delete this song? This removes all its files and cannot be undone.")) return;
|
||||
formError.textContent = "";
|
||||
const resp = await fetch(`/api/jobs/${id}`, { method: "DELETE" });
|
||||
if (!resp.ok) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
formError.textContent = body.detail || `error: ${resp.status}`;
|
||||
return;
|
||||
}
|
||||
if (selectedId === id) selectedId = null;
|
||||
refreshJobs();
|
||||
}
|
||||
|
||||
const ACTION_LABELS = {
|
||||
rerun: "RERUN",
|
||||
edit: "EDIT & REBUILD",
|
||||
"rebuild-full": "REBUILD FROM SCRATCH",
|
||||
delete: "DELETE",
|
||||
};
|
||||
|
||||
const ACTION_HANDLERS = {
|
||||
rerun: rerunJob,
|
||||
edit: startRebuild,
|
||||
"rebuild-full": rebuildFullJob,
|
||||
delete: deleteJob,
|
||||
};
|
||||
|
||||
function actionButton(action, jobId) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "action";
|
||||
button.dataset.action = action;
|
||||
button.textContent = action === "rerun" ? "RERUN" : "EDIT & REBUILD";
|
||||
button.addEventListener("click",
|
||||
() => action === "rerun" ? rerunJob(jobId) : startRebuild(jobId));
|
||||
button.textContent = ACTION_LABELS[action];
|
||||
button.addEventListener("click", () => ACTION_HANDLERS[action](jobId));
|
||||
return button;
|
||||
}
|
||||
|
||||
function renderActions(job) {
|
||||
// no controls mid-run: both endpoints answer 409 until the job settles
|
||||
const wanted = job.status === "failed" ? ["rerun", "edit"]
|
||||
: job.status === "done" ? ["edit"] : [];
|
||||
// no controls mid-run: every endpoint answers 409 until the job settles
|
||||
const doneActions = ["edit", "rebuild-full", "delete"];
|
||||
const wanted = job.status === "failed" ? ["rerun", ...doneActions]
|
||||
: job.status === "done" ? doneActions : [];
|
||||
const shown = [...reviewActions.children].map(b => b.dataset.action);
|
||||
// rebuilding an unchanged row would detach a focused action on every poll
|
||||
if (reviewActions.dataset.jobId === job.id && shown.join() === wanted.join()) return;
|
||||
|
||||
@@ -532,6 +532,44 @@ def _rebuild_flaws(html):
|
||||
return flaws
|
||||
|
||||
|
||||
def _rebuild_full_and_delete_flaws(html):
|
||||
flaws = []
|
||||
actions = _fn_body(html, "renderActions")
|
||||
for action in ('"rebuild-full"', '"delete"'):
|
||||
if action not in actions:
|
||||
flaws.append(f"{action} is not offered from renderActions")
|
||||
|
||||
rf = _fn_body(html, "rebuildFullJob")
|
||||
if "/rebuild-full" not in rf or 'method: "POST"' not in rf:
|
||||
flaws.append("rebuildFullJob does not POST to /rebuild-full")
|
||||
|
||||
d = _fn_body(html, "deleteJob")
|
||||
confirm_idx = d.find("confirm(")
|
||||
fetch_idx = d.find("fetch(")
|
||||
if confirm_idx == -1:
|
||||
flaws.append("deleteJob does not confirm before deleting")
|
||||
elif fetch_idx != -1 and confirm_idx > fetch_idx:
|
||||
flaws.append("deleteJob fetches before confirming")
|
||||
if 'method: "DELETE"' not in d:
|
||||
flaws.append("deleteJob does not send a DELETE request")
|
||||
return flaws
|
||||
|
||||
|
||||
def test_rebuild_full_and_delete_controls_are_wired_up():
|
||||
assert _rebuild_full_and_delete_flaws(_index_html()) == []
|
||||
|
||||
|
||||
def _empty_state_flaws(html):
|
||||
refresh = _fn_body(html, "refreshJobs")
|
||||
if "clearDetail()" not in refresh:
|
||||
return ["refreshJobs never resets the panel when no job is selected"]
|
||||
return []
|
||||
|
||||
|
||||
def test_review_panel_resets_when_no_job_is_selected():
|
||||
assert _empty_state_flaws(_index_html()) == []
|
||||
|
||||
|
||||
# each mutation is a plausible regression; the checker above must reject all of them
|
||||
MUTATIONS = [
|
||||
(_focus_flaws, "queue: card moved unconditionally",
|
||||
@@ -560,6 +598,14 @@ MUTATIONS = [
|
||||
(_rebuild_flaws, "rebuild: action row rebuilt on every poll",
|
||||
"if (reviewActions.dataset.jobId === job.id && shown.join() === wanted.join()) return;",
|
||||
""),
|
||||
(_rebuild_full_and_delete_flaws, "delete: confirmation dialog removed",
|
||||
'if (!confirm("Delete this song? This removes all its files and cannot be undone.")) return;',
|
||||
""),
|
||||
(_rebuild_full_and_delete_flaws, "rebuild-full: no longer offered on a done job",
|
||||
'["edit", "rebuild-full", "delete"]', '["edit", "delete"]'),
|
||||
(_empty_state_flaws, "empty state: clearDetail no longer wired into refreshJobs",
|
||||
"if (selectedId) refreshDetail();\n else clearDetail();",
|
||||
"if (selectedId) refreshDetail();"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user