diff --git a/docs/GUI.md b/docs/GUI.md index debe7b7..db1da59 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -266,7 +266,8 @@ SPEED (2026-10-22, my request: "the apply page takes quite a while to load") taste model trains again only when your ratings or a job's text change (jobs.taste_for). The lists are made ahead at start and a few seconds after your last decision (warm_review). (2026-10-22, my request: Review still took 0.5-3 s) Review asks with quick=1: the list made last comes at once (about 10 ms), never waiting on Google or a rebuild; when - something changed since (stale) the page asks again without quick and swaps the new list in without moving you. A job + something changed since (stale) the page asks again without quick and swaps the new list in: once you moved (decided, skipped, went back) it keeps + your place; before that it starts at the new list's first job (a quick list's first may no longer come first). A job decided here after a list was made is left out of it (gui._decided). The monitor makes every kept list again when its rows, data/ or settings changed (warm_changed, every 15 s), so stale is rare. Each source (co-ops / internships) has its own kept list. diff --git a/src/nuauto/gui.py b/src/nuauto/gui.py index 3c3ed71..b80feba 100644 --- a/src/nuauto/gui.py +++ b/src/nuauto/gui.py @@ -492,7 +492,11 @@ def monitor(self): def run_now(self, groups): for g in groups: if g in self.GROUPS and group_on(g): - threading.Thread(target=self.run_group, args=(g,), daemon=True).start() + # the sheet: a read already under way may have started before the change being checked, so wait for + # it and read again (run_group would skip) + kw = {"target": self.refresh_rows, "kwargs": {"force": True}} if g == "sheet" else \ + {"target": self.run_group, "args": (g,)} + threading.Thread(daemon=True, **kw).start() def quit(self): if self.stopping: diff --git a/src/nuauto/gui_static/app.js b/src/nuauto/gui_static/app.js index a8c120c..b743d78 100644 --- a/src/nuauto/gui_static/app.js +++ b/src/nuauto/gui_static/app.js @@ -671,6 +671,7 @@ screens.review = async (view) => { const interns = (S.state && S.state.internships) || { on: false }; let source = interns.on && sessionStorage.getItem("reviewSource") === "intern" ? "intern" : "nuworks"; let jobs = [], i = 0, card = null, history = {}, shown = 0, total = 0, resumeId = null, listed = 0, loadSeq = 0; + let moved = false; // you went to another job (or decided one) since the list came: a newer list keeps your place let q = sessionStorage.getItem("reviewQ") || "", category = sessionStorage.getItem("reviewCat") || ""; const cards = new Map(); // job id -> promise of its card (the next ones are fetched ahead) let queue = Promise.resolve(); // decisions are sent to the server one at a time, behind the screen @@ -752,12 +753,14 @@ screens.review = async (view) => { if (mine !== loadSeq || !body.isConnected) return; jobs = r.jobs; total = r.total; i = 0; listed = 0; cards.clear(); drawCategories(r.categories); if (!q && resumeId) { i = Math.max(0, jobs.findIndex((x) => x.id === resumeId)); resumeId = null; } + moved = i > 0; show(); if (r.stale) refresh(mine, params); } // The list made from what changed since the quick one: the job on screen stays (one that left the list: the next one // still in it), a search keeps how many it showed and where you scrolled, and the same jobs draw nothing again. + // Not moved yet: the new list from its first job (the quick list's first may no longer come first). async function refresh(mine, params) { let r; try { await queue; r = await api.get(`/api/review?${params}`); } catch (e) { return; } @@ -770,7 +773,7 @@ screens.review = async (view) => { jobs = r.jobs; if (listMode()) { const had = listed, y = window.scrollY; showList(true, had); window.scrollTo(0, y); return; } const at = new Map(jobs.map((j, k) => [j.id, k])); - const next = old.slice(i).find((j) => at.has(j.id)); + const next = moved ? old.slice(i).find((j) => at.has(j.id)) : jobs[0]; const same = next && old[i] && next.id === old[i].id && !changed(jobs[at.get(next.id)]); i = next ? at.get(next.id) : jobs.length; if (same) position(); else show(); // (the same job, unchanged: its card stays as it is) @@ -798,7 +801,7 @@ screens.review = async (view) => { el("p", { class: "muted", text: mode !== "approve" ? "Ratings teach the ranking your taste (it needs 5 yes and 5 no)." : toSites ? `Approved internships wait in Apply > ${otherTabLabel()}.` : "Approved jobs are in your sheet. Apply to them next." }), - el("div", { class: "row" }, jobs.length ? el("button", { class: "btn", text: "Back one", onclick: () => { i = Math.max(0, i - 1); show(); } }) : null, + el("div", { class: "row" }, jobs.length ? el("button", { class: "btn", text: "Back one", onclick: () => { moved = true; i = Math.max(0, i - 1); show(); } }) : null, el("a", { class: "btn primary", href: toSites ? "#/other" : mode === "approve" ? "#/apply" : "#/home", text: mode === "approve" ? "Go to Apply" : "Done" })))); return; @@ -878,6 +881,7 @@ screens.review = async (view) => { function decide(decision) { const j = jobs[i]; if (!j) return; + moved = true; if (decision === "skip") { i++; return show(); } if (decision === "back") { i = Math.max(0, i - 1); return show(); } const index = i; diff --git a/tests/test_gui.py b/tests/test_gui.py index afd8f5b..4451314 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1135,8 +1135,10 @@ def review_again(): # a new Review screen: its cards are fetched again, its lis # a new Review: it shows first, flagged; Approve (y) turns that same row Approved again (no second row) review_again() page.wait_for_function(harbor_first) + # (the list made last may come first, its card from before the undo: the new list's card follows) + page.locator("[data-testid=job-card] .facts", has_text=f"already Proposed (Other jobs row {harbor_row})").wait_for() facts = page.inner_text("[data-testid=job-card] .facts") - assert f"already Proposed (Other jobs row {harbor_row})" in facts and "your Proposed row in the Other jobs tab" in facts, facts + assert "your Proposed row in the Other jobs tab" in facts, facts page.keyboard.press("y") page.locator("[data-testid=toast]", has_text="Approved: adding it to the Other jobs tab.").last.wait_for() again = eventually(lambda: (r := other_row(HARBOR)) and r["status"] == "Approved" and r, "the same row Approved again")