Merge pull request 'webui: mark songs with hand edits in the queue and REBUILD ALL FROM…' (#32) from edited-marks into main
Reviewed-on: #32 Reviewed-by: branderson <brad@branderson.io>
This commit is contained in:
@@ -463,6 +463,11 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.lineage:hover { border-bottom-color: var(--aqua); }
|
.lineage:hover { border-bottom-color: var(--aqua); }
|
||||||
.job-mark { grid-area: 1 / 2; justify-self: end; font-size: 0.8rem; color: var(--text-muted); }
|
.job-mark { grid-area: 1 / 2; justify-self: end; font-size: 0.8rem; color: var(--text-muted); }
|
||||||
|
.job-edits { grid-area: 2 / 2; justify-self: end; display: flex; gap: 6px; font-size: 0.72rem; }
|
||||||
|
.job-edits-kept { color: var(--aqua); }
|
||||||
|
.job-edits-stale { color: var(--warn); }
|
||||||
|
.rebuild-all-kept { color: var(--aqua); }
|
||||||
|
.rebuild-all-aside { color: var(--warn); }
|
||||||
|
|
||||||
#detail {
|
#detail {
|
||||||
margin: 13px 0 0;
|
margin: 13px 0 0;
|
||||||
@@ -584,7 +589,8 @@ button { cursor: pointer; }
|
|||||||
<form id="rebuild-all-form" method="dialog">
|
<form id="rebuild-all-form" method="dialog">
|
||||||
<h2 id="rebuild-all-heading">REBUILD ALL SONGS</h2>
|
<h2 id="rebuild-all-heading">REBUILD ALL SONGS</h2>
|
||||||
<p>Reruns the songs ticked below from the chosen stage onwards with the
|
<p>Reruns the songs ticked below from the chosen stage onwards with the
|
||||||
inputs they have now, for when a songtrace update improves that stage.</p>
|
inputs they have now, for when a songtrace update improves that stage.
|
||||||
|
Lines edited by hand are kept.</p>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Restart from</span>
|
<span>Restart from</span>
|
||||||
<select name="force_from" required></select>
|
<select name="force_from" required></select>
|
||||||
@@ -829,6 +835,18 @@ function rebuildAllRow(song) {
|
|||||||
undefined, { dateStyle: "medium", timeStyle: "short" });
|
undefined, { dateStyle: "medium", timeStyle: "short" });
|
||||||
meta.textContent = busy ? "in progress, skipped"
|
meta.textContent = busy ? "in progress, skipped"
|
||||||
: `${song.status === "failed" ? "failed" : song.rebuilt_from ? "rebuilt" : "built"} ${when || ""}`.trim();
|
: `${song.status === "failed" ? "failed" : song.rebuilt_from ? "rebuilt" : "built"} ${when || ""}`.trim();
|
||||||
|
if (song.edited_lines) {
|
||||||
|
const kept = document.createElement("span");
|
||||||
|
kept.className = "rebuild-all-kept";
|
||||||
|
kept.textContent = ` · ${song.edited_lines} edited line${song.edited_lines === 1 ? "" : "s"} kept`;
|
||||||
|
meta.append(kept);
|
||||||
|
}
|
||||||
|
if (song.stale_edits) {
|
||||||
|
const aside = document.createElement("span");
|
||||||
|
aside.className = "rebuild-all-aside";
|
||||||
|
aside.textContent = ` · ${song.stale_edits} set aside`;
|
||||||
|
meta.append(aside);
|
||||||
|
}
|
||||||
row.append(box, name, meta);
|
row.append(box, name, meta);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -904,7 +922,10 @@ function jobCard(job) {
|
|||||||
markLabel.className = "sr-only";
|
markLabel.className = "sr-only";
|
||||||
mark.append(markGlyph, markLabel);
|
mark.append(markGlyph, markLabel);
|
||||||
|
|
||||||
card.append(title, artist, stateLine, time, mark);
|
const edits = document.createElement("span");
|
||||||
|
edits.className = "job-edits";
|
||||||
|
|
||||||
|
card.append(title, artist, stateLine, time, mark, edits);
|
||||||
card.addEventListener("click", () => selectJob(job.id));
|
card.addEventListener("click", () => selectJob(job.id));
|
||||||
paintCard(card, job);
|
paintCard(card, job);
|
||||||
return card;
|
return card;
|
||||||
@@ -912,7 +933,7 @@ function jobCard(job) {
|
|||||||
|
|
||||||
function paintCard(card, job) {
|
function paintCard(card, job) {
|
||||||
const state = STATE[job.status] ?? { glyph: "·", cls: "st-queued" };
|
const state = STATE[job.status] ?? { glyph: "·", cls: "st-queued" };
|
||||||
const [title, artist, stateLine, time, mark] = card.children;
|
const [title, artist, stateLine, time, mark, edits] = card.children;
|
||||||
card.className = `job ${state.cls}`;
|
card.className = `job ${state.cls}`;
|
||||||
if (job.id === selectedId) card.setAttribute("aria-current", "true");
|
if (job.id === selectedId) card.setAttribute("aria-current", "true");
|
||||||
else card.removeAttribute("aria-current");
|
else card.removeAttribute("aria-current");
|
||||||
@@ -923,6 +944,33 @@ function paintCard(card, job) {
|
|||||||
time.textContent = elapsed(job);
|
time.textContent = elapsed(job);
|
||||||
mark.children[0].textContent = job.rebuilt_from ? "↻" : "";
|
mark.children[0].textContent = job.rebuilt_from ? "↻" : "";
|
||||||
mark.children[1].textContent = job.rebuilt_from ? " rebuilt" : "";
|
mark.children[1].textContent = job.rebuilt_from ? " rebuilt" : "";
|
||||||
|
paintEdits(edits, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hand edits a rebuild keeps, and any set aside because the lyrics changed
|
||||||
|
function paintEdits(node, job) {
|
||||||
|
const parts = [];
|
||||||
|
if (job.edited_lines) parts.push(["job-edits-kept", `✎ ${job.edited_lines}`]);
|
||||||
|
if (job.stale_edits) parts.push(["job-edits-stale", `⚠ ${job.stale_edits}`]);
|
||||||
|
const label = [
|
||||||
|
job.edited_lines ? `${job.edited_lines} edited line${job.edited_lines === 1 ? "" : "s"}` : "",
|
||||||
|
job.stale_edits ? `${job.stale_edits} set aside` : "",
|
||||||
|
].filter(Boolean).join(", ");
|
||||||
|
if (node.title === label) return;
|
||||||
|
node.title = label;
|
||||||
|
node.replaceChildren(...parts.map(([cls, text]) => {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = cls;
|
||||||
|
span.setAttribute("aria-hidden", "true");
|
||||||
|
span.textContent = text;
|
||||||
|
return span;
|
||||||
|
}));
|
||||||
|
if (label) {
|
||||||
|
const sr = document.createElement("span");
|
||||||
|
sr.className = "sr-only";
|
||||||
|
sr.textContent = label;
|
||||||
|
node.append(sr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// patched in place rather than replaced wholesale: detaching a card would blur
|
// patched in place rather than replaced wholesale: detaching a card would blur
|
||||||
|
|||||||
+17
-2
@@ -340,11 +340,21 @@ def create_app(cfg: Config, runner=run_pipeline) -> FastAPI:
|
|||||||
position = {job_id: n for n, job_id in enumerate(order)}
|
position = {job_id: n for n, job_id in enumerate(order)}
|
||||||
return sorted(latest.values(), key=lambda j: position[j.id], reverse=True)
|
return sorted(latest.values(), key=lambda j: position[j.id], reverse=True)
|
||||||
|
|
||||||
|
def _edit_counts(job: Job) -> dict:
|
||||||
|
"""How much of the song was edited by hand, from its edits file."""
|
||||||
|
path = cfg.work_dir / song_slug(job.inp) / edits.EDITS_FILE
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return {"edited_lines": 0, "stale_edits": 0}
|
||||||
|
return {"edited_lines": len(data.get("lines", {})),
|
||||||
|
"stale_edits": len(data.get("stale", []))}
|
||||||
|
|
||||||
@app.get("/api/rebuild-all")
|
@app.get("/api/rebuild-all")
|
||||||
def rebuild_all_candidates():
|
def rebuild_all_candidates():
|
||||||
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
||||||
"status": j.status, "rebuilt_from": j.rebuilt_from,
|
"status": j.status, "rebuilt_from": j.rebuilt_from,
|
||||||
"finished_at": j.finished_at}
|
"finished_at": j.finished_at, **_edit_counts(j)}
|
||||||
for j in _current_songs()]
|
for j in _current_songs()]
|
||||||
|
|
||||||
@app.post("/api/rebuild-all", status_code=201)
|
@app.post("/api/rebuild-all", status_code=201)
|
||||||
@@ -380,11 +390,16 @@ def create_app(cfg: Config, runner=run_pipeline) -> FastAPI:
|
|||||||
|
|
||||||
@app.get("/api/jobs")
|
@app.get("/api/jobs")
|
||||||
def list_jobs():
|
def list_jobs():
|
||||||
|
# edits belong to the song as it is now, so only the job standing for
|
||||||
|
# it carries them; the runs it replaced would claim them twice
|
||||||
|
counts = {j.id: _edit_counts(j) for j in _current_songs()}
|
||||||
|
none = {"edited_lines": 0, "stale_edits": 0}
|
||||||
with lock:
|
with lock:
|
||||||
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
||||||
"status": j.status, "stage": j.stage,
|
"status": j.status, "stage": j.stage,
|
||||||
"created_at": j.created_at, "started_at": j.started_at,
|
"created_at": j.created_at, "started_at": j.started_at,
|
||||||
"finished_at": j.finished_at, "rebuilt_from": j.rebuilt_from}
|
"finished_at": j.finished_at, "rebuilt_from": j.rebuilt_from,
|
||||||
|
**counts.get(j.id, none)}
|
||||||
for j in (jobs[i] for i in reversed(order))]
|
for j in (jobs[i] for i in reversed(order))]
|
||||||
|
|
||||||
@app.get("/api/jobs/{job_id}")
|
@app.get("/api/jobs/{job_id}")
|
||||||
|
|||||||
@@ -1977,3 +1977,40 @@ def test_a_stale_edit_can_be_discarded(tmp_path):
|
|||||||
assert resp.status_code == 200 and resp.json()["stale"] == []
|
assert resp.status_code == 200 and resp.json()["stale"] == []
|
||||||
assert not (work / edits.EDITS_FILE).exists()
|
assert not (work / edits.EDITS_FILE).exists()
|
||||||
assert client.delete(f"/api/jobs/{second}/stale/0").status_code == 404
|
assert client.delete(f"/api/jobs/{second}/stale/0").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def _listed(client, job_id):
|
||||||
|
return next(j for j in client.get("/api/jobs").json() if j["id"] == job_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_queue_reports_a_songs_edited_lines(tmp_path):
|
||||||
|
client, _ = make_client(tmp_path, chart_runner)
|
||||||
|
job_id = make_charted_job(client)
|
||||||
|
assert _listed(client, job_id)["edited_lines"] == 0
|
||||||
|
client.put(f"/api/jobs/{job_id}/lines/1", json=EDIT)
|
||||||
|
assert _listed(client, job_id)["edited_lines"] == 1
|
||||||
|
assert _listed(client, job_id)["stale_edits"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_counts_follow_the_song_to_its_latest_run(tmp_path):
|
||||||
|
client, _ = make_client(tmp_path, chart_runner)
|
||||||
|
first = make_charted_job(client)
|
||||||
|
client.put(f"/api/jobs/{first}/lines/1", json=EDIT)
|
||||||
|
rerun = client.post(f"/api/jobs/{first}/rerun").json()["id"]
|
||||||
|
wait_status(client, rerun, "done")
|
||||||
|
assert _listed(client, rerun)["edited_lines"] == 1
|
||||||
|
assert _listed(client, first)["edited_lines"] == 0
|
||||||
|
candidates = client.get("/api/rebuild-all").json()
|
||||||
|
assert [(c["id"], c["edited_lines"]) for c in candidates] == [(rerun, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_queue_reports_edits_set_aside(tmp_path):
|
||||||
|
client, _ = make_client(tmp_path, chart_runner)
|
||||||
|
first = make_charted_job(client)
|
||||||
|
client.put(f"/api/jobs/{first}/lines/1", json=EDIT)
|
||||||
|
second = make_charted_job(client, lyrics="one\nthree more")
|
||||||
|
# the next save looks at the edits against the new lyrics, as a rebuild does
|
||||||
|
client.put(f"/api/jobs/{second}/lines/0", json={"notes": [
|
||||||
|
{"start": 1.0, "duration": 0.5, "pitch": 0, "text": "one"}]})
|
||||||
|
listed = _listed(client, second)
|
||||||
|
assert (listed["edited_lines"], listed["stale_edits"]) == (1, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user