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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ Status values: Proposed, Approved, Applied, Failed, Needs Human, Accepted, Rejec
leave the NUworks tab (they count toward the caps); an Applied Other job may move to NUworks (then it counts
there; my call 2026-10-09). Unresolved Submit clicks are never moved or removed.
Google access is OAuth only, never service accounts (docs/DEPLOY.md, GOOGLE SHEETS LOGIN). Google allows about 60 reads a
minute: every client waits and retries on "too many requests" (gspread.BackOffHTTPClient), and the internship Review reuses
the Other jobs rows for 30 s (each change NUauto makes forgets them).
minute: every client waits and retries on "too many requests" (sheet.HTTPClient, gspread's BackOffHTTPClient); the opened
sheet is kept 5 minutes (sheet.OPEN; rows are always read fresh), and Apply and the internship Review reuse the Other jobs
rows for 30 s (each change NUauto makes forgets them; docs/GUI.md, SPEED).

BROWSER (Playwright, Firefox)
- Deterministic script, not an AI clicking around freely. Slow, human-like pacing.
Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ MODULES (src/nuauto/)
app: bundle paths, app-data STATE_DIR, local mode, Playwright's browser cache), self_cmd() / self_exe()
(how NUauto runs itself). No logic beyond that.
sheet.py the Google Sheet. client() / open_worksheet(interactive) (interactive=False never opens a
login page: NotLoggedIn), read_rows(), check_limits() (MAX_PER_WEEK/MAX_TOTAL), set_status(),
login page: NotLoggedIn; the opened sheet and tabs kept OPEN_FOR s: OPEN, forget(), HTTPClient), read_rows(), check_limits() (MAX_PER_WEEK/MAX_TOTAL), set_status(),
mark_submit_started()/resolve_submit(), mark_applied_by_hand(), mark_site_done(), append_note(),
add_proposed(), unapprove() (GUI Undo), create_sheet() (Sheets API), google_login().
jobs.py the job pool + viewers. PREFS (local_config "preferences" over DEFAULTS), RULES, set_prefs(),
Expand Down Expand Up @@ -190,7 +190,9 @@ changes the sheet). test_browser.py covers the domain lock and the profile lock.
company-site agent's guard. test_mail.py covers the email codes and links (Gmail's message format, which mail counts, the
agent's `email` command). test_insights.py covers the Insights numbers (pay parsing, folding, no guessing). test_intern.py covers the
internship pool and postings.py (job-site links, parsing, the network guard on a local server, fetch tries, rules,
Review queue, a whole update with Claude stood in for). test_deploy.py runs deploy.py on a throwaway git repo (copy, web restart only when needed, undo on failure). test_demo.py runs the real apply.py on demo mode's fake pages (stops, cap, re-login).
Review queue (the taste model trains again only when ratings or a job's text change), a whole update with Claude
stood in for). test_sheet.py also covers the kept sheet (sheet.OPEN: opened once, opened again on a new login, an
expiring token, a refused request, a renamed tab). test_deploy.py runs deploy.py on a throwaway git repo (copy, web restart only when needed, undo on failure). test_demo.py runs the real apply.py on demo mode's fake pages (stops, cap, re-login).
test_gui.py covers the GUI server's security and flows; test_homelab_app.py the same app as the homelab serves it (demo
`--remote`: pairing, refusals, sessions, queue, devices, a phone-width browser); test_link.py the laptop's link to it (pairing,
calls, the /api/homelab/ routes, an Apply run there, unpairing; then `--demo --linked` in a browser); tests/screens.py (not a test:
Expand Down
6 changes: 5 additions & 1 deletion docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ GOOGLE SHEETS LOGIN (gspread + OAuth)
- Testing mode -> a login lasts 7 days. Re-login on the laptop: `nuauto login google`; it
pushes the new token to the homelab. Discord warns the day before; laptop commands re-open
the Google login by themselves when it has expired.
- Open the sheet with open_by_key(SHEET_ID) so only the Sheets API is needed.
- Open the sheet with open_by_key(SHEET_ID) so only the Sheets API is needed. Each process keeps the opened sheet
and its tabs 5 minutes (sheet.OPEN, OPEN_FOR; rows are always read fresh), so a read is one request: opened again
when the sheet ID or the login files change, when the access token is about to expire (an expired login is still
NotLoggedIn), and after a request Google refuses (sheet.HTTPClient, gspread's BackOffHTTPClient: it also waits and
retries on "too many requests").
- Optional: the assistant's email codes (mail.py) need the Gmail API enabled in the same project
(console.cloud.google.com/apis/library/gmail.googleapis.com), then `nuauto login gmail` on the laptop: Google's
read-only Gmail permission, a token of its own (local/gmail_token.json), so the Sheets token never reads mail. It
Expand Down
25 changes: 21 additions & 4 deletions docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,12 @@ SCREENS
and where you answer it (its screen; a laptop session: its terminal window).
GET /api/jobs?tab= is one tab's every row in its sections with what Move / Remove allow (gui.jobs_tab);
/api/apply, /api/company, /api/other, /api/sheet stay (the laptop's link reads the homelab's /api/company).
The lists follow the sheet by themselves: /api/state carries rows_at (when the sheet was last read), the
server reads it again after every finished row and run and when it is 30 s old, and the open screen
reloads its lists when rows_at changes (no closing and reopening). "At most" keeps what you typed. The menu's
The lists follow the sheet by themselves: /api/state carries sheet_version (+1 each time a read finds
either tab changed), the server reads both tabs again after every finished row and run, when an
assistant session starts or ends, and when they are 30 s old, and the open screen reloads its lists when
sheet_version changes (no closing and reopening; each /api/jobs answer carries the version its rows are as
new as, so the screen's own reload is not followed by a second one that would close an open menu). Opening the screen is answered from the rows read last
and reads both tabs again behind it (SPEED). Each tab is drawn as soon as its own answer comes. "At most" keeps what you typed. The menu's
count is Approved + the assistant's rows + retries. #/sheet (the old Sheet screen) opens Apply.
Insights NUworks co-ops / "<term> internships" switch when internships are on (2026-10-16; remembered for the
session): each its own pool, applications and sheet tab (insights.collect / collect_intern: the internship
Expand Down Expand Up @@ -224,6 +227,20 @@ SETUP WIZARD (onboard.py + gui_static/setup.js; each step checks itself)
homelab optional, a source install only: the same Homelab card as Settings (done when NUauto runs with one;
then its NUauto on your homelab part shows there too: one step for all of it)

SPEED (2026-10-22, my request: "the apply page takes quite a while to load")
- Google Sheets: the opened spreadsheet and its tabs are kept 5 minutes (sheet.OPEN; a read is one trip to Google,
about 0.25 s, not three); rows are always read fresh. Opened again on a new Google login, an access token about to
expire, or a refused request (a tab renamed or deleted: read_rows opens it again once). docs/DEPLOY.md.
- Apply: answered from the rows read last (the NUworks tab's: up to 60 s; the Other jobs tab's: up to 30 s,
gui.other_rows); opening the screen (recheck=1) reads both again in the background, and a change reloads the lists.
- Review: the list (pool, taste model, order) is kept until data/, your settings or the sheet's rows change
(gui.review_lists, keyed by every data/ file's size and time: a few ms); searches and filters only narrow it. The
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), so the first open and the next search are instant.
- Buttons: one whose click starts something slower shows it at once (aria-busy) and takes no second click until it
is done (busyWhile); sheet changes from a row's menu show "Updating the sheet…" until the answer comes. Switching
screens shows Loading… at once; a screen you already left never takes over.

HEALTH (health.py; the same checks `nuauto doctor` prints, `nuauto doctor --json`)
check how GUI runs it
files settings saved, local/ 700 and secrets 600 (Fix permissions) every minute
Expand Down Expand Up @@ -335,7 +352,7 @@ VIEW MODE (`nuauto gui --view`; gui.VIEW; added 2026-10-09 so an agent can look
(a newer push to a pull request stops its older run).

API (JSON; all need the cookie; POSTs need X-NUauto: 1)
GET /api/state counts, week, to-do, last scan, setup_needed, the current task; internships {on, term,
GET /api/state counts, week, to-do, last scan, setup_needed, the current task, sheet_version; internships {on, term,
review: how many wait for Review}; where (laptop | homelab), laptop_only {action: why} (the
homelab's page), remote_app (the laptop: the homelab's page address, when it has one); todo:
the assistants that wait for you first ({"kind": "assistant", "status", "text", "screen": its
Expand Down
3 changes: 2 additions & 1 deletion docs/PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ RANKING (my settings: the engineering field)
MY RATINGS (taste model)
- `nuauto rate` (y/n, nothing goes in the sheet) -> data/ratings.json, which the laptop owns
and pushes to the homelab. They train a TF-IDF model that reorders the pool (half rank,
half my taste) once there are 5 yes + 5 no.
half my taste) once there are 5 yes + 5 no. Trained again only when my ratings or a job's text change
(jobs.taste_for keeps the last few; training takes about a second).
- Rating order rotates categories (security, embedded, hardware, systems, robotics/test,
web/full-stack, software, data/ML, IT, other, front-end) so every category gets rated; once the model
trains, it also mixes in the jobs it is least sure about.
Expand Down
7 changes: 5 additions & 2 deletions src/nuauto/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ def _locked(self):
return fd

def _load(self):
with open(self.path) as f:
return json.load(f)
try:
with open(self.path) as f:
return json.load(f)
except FileNotFoundError: # the tab is gone (a reset, a test): what Google answers for a deleted tab
raise gspread.exceptions.APIError(FakeResponse({"error": {"code": 400, "message": f"demo: no tab {self.title!r}"}}))

def get_all_values(self):
rows = [list(r) for r in self._load()["rows"]]
Expand Down
Loading
Loading