Merge remote-tracking branch 'origin/main' into editor-merge
# Conflicts: # tests/test_web.py
This commit is contained in:
@@ -623,6 +623,11 @@ button { cursor: pointer; }
|
||||
}
|
||||
.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-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 {
|
||||
margin: 13px 0 0;
|
||||
@@ -744,7 +749,8 @@ button { cursor: pointer; }
|
||||
<form id="rebuild-all-form" method="dialog">
|
||||
<h2 id="rebuild-all-heading">REBUILD ALL SONGS</h2>
|
||||
<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">
|
||||
<span>Restart from</span>
|
||||
<select name="force_from" required></select>
|
||||
@@ -991,6 +997,18 @@ function rebuildAllRow(song) {
|
||||
undefined, { dateStyle: "medium", timeStyle: "short" });
|
||||
meta.textContent = busy ? "in progress, skipped"
|
||||
: `${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);
|
||||
return row;
|
||||
}
|
||||
@@ -1066,7 +1084,10 @@ function jobCard(job) {
|
||||
markLabel.className = "sr-only";
|
||||
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));
|
||||
paintCard(card, job);
|
||||
return card;
|
||||
@@ -1074,7 +1095,7 @@ function jobCard(job) {
|
||||
|
||||
function paintCard(card, job) {
|
||||
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}`;
|
||||
if (job.id === selectedId) card.setAttribute("aria-current", "true");
|
||||
else card.removeAttribute("aria-current");
|
||||
@@ -1085,6 +1106,33 @@ function paintCard(card, job) {
|
||||
time.textContent = elapsed(job);
|
||||
mark.children[0].textContent = job.rebuilt_from ? "↻" : "";
|
||||
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
|
||||
|
||||
+17
-2
@@ -342,11 +342,21 @@ def create_app(cfg: Config, runner=run_pipeline) -> FastAPI:
|
||||
position = {job_id: n for n, job_id in enumerate(order)}
|
||||
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")
|
||||
def rebuild_all_candidates():
|
||||
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
||||
"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()]
|
||||
|
||||
@app.post("/api/rebuild-all", status_code=201)
|
||||
@@ -382,11 +392,16 @@ def create_app(cfg: Config, runner=run_pipeline) -> FastAPI:
|
||||
|
||||
@app.get("/api/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:
|
||||
return [{"id": j.id, "title": j.title, "artist": j.artist,
|
||||
"status": j.status, "stage": j.stage,
|
||||
"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))]
|
||||
|
||||
@app.get("/api/jobs/{job_id}")
|
||||
|
||||
@@ -2074,3 +2074,40 @@ def test_a_bad_break_is_refused(tmp_path):
|
||||
job_id = make_charted_job(client)
|
||||
for body in ({"0": 1.0}, {"9": 1.0}, {"1": -2}, {"1": "x"}):
|
||||
assert client.put(f"/api/jobs/{job_id}/breaks", json=body).status_code == 400
|
||||
|
||||
|
||||
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