Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/nuauto/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions src/nuauto/gui_static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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; }
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion tests/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading