Repository navigation
feat(status): list Harper jobs on a Jobs tab, and follow a CSV import's job in a toast - #1880
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a new 'Jobs' tab to the instance Status dashboard, allowing users to view background operations such as CSV/S3 loads, exports, bulk deletes, and backups. It features independent polling, permission gating for super users, and node-specific job listings for clusters. Additionally, it integrates a progress toast for tracking data imports and includes comprehensive test coverage. Feedback on the changes suggests a UX improvement for the Jobs table to prevent row toggling when a user is actively selecting text inside a row.
This was referenced Oct 11, 2026
dawsontoth
marked this pull request as ready for review
October 12, 2026 18:11
…'s job in a toast Adds a Jobs tab to the instance Status page that lists the jobs Harper ran in the selected window (search_jobs_by_start_date), newest first, with an expandable row for each job's full record. It polls on the page's refresh interval through pollUnlessForbidden, is offered only to roles that can run the operation, and sits outside the get_analytics capability gate. The operation takes no limit, so Jobs offers windows up to 7 days and renders the 200 newest rows. CSV imports now show their job in a loading toast that updates on every get_job poll (queued/running and elapsed time) and becomes the result. The toast is settled from the mutation's own callbacks, so it still resolves if the modal unmounts mid-import, and a throwing listener cannot turn a completed import into a reported failure. Refs #1266 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tails A drag that selects text in a summary row (the user, type or message) ends in a click on the row, so selecting a job's text opened or closed its details. The row now skips the toggle when the selection is non-empty and starts or ends inside it. The check is scoped to the row rather than to any page selection, so a click right after selecting text elsewhere still toggles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth
force-pushed
the
claude/1266-status-jobs
branch
from
October 12, 2026 19:47
a661b95 to
39edbab
Compare
… is picked for it Each Jobs poll makes Harper run an unlimited select over system.hdb_job for the window, so Jobs now fetches when it opens and on Refresh now, and polls only on an interval chosen on the tab. While it is off, a refocus or reconnect does not refetch either. The interval lives in its own jobsRefresh search param, which defaults to off. It is separate from the charts' refresh because one URL value cannot default to 60s on the charts and to off on Jobs: a tab switch would either start Jobs polling or reset the charts' choice. Every Status navigation carries both unchanged, so the charts' defaults and round trips are as before. Push updates are tracked in HarperFast/harper#3179. Refs #1266 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
Studio has no view of Harper's jobs — the background runs behind CSV/S3 loads, exports, bulk deletes and backups — so there is no way to see what ran, what failed, or why. A CSV import also blocks behind an "Importing…" spinner that shows neither whether Harper has started the job nor how long it has been running (#1266).
💡 Solution
A Jobs tab on the instance Status page. It lists jobs started in the selected window, newest first, via
search_jobs_by_start_date, with status, type, start time, duration, user and message. Each row expands to show the full record. Harper sometimes records a job's message as an object (a refused bulk load's permission report), so the expanded row renders it as JSON text, never[object Object]. The tab:pollUnlessForbidden, so a 403 stops the timer. That interval is its ownjobsRefreshsearch parameter, which defaults to off. Jobs shares the existingrangeparameter.get_analytics, so it joins Overview inANALYTICS_FREE_TABS: a failed analytics probe no longer redirects?tab=jobsto Overview or swaps in the "analytics unavailable" notice.search_jobs_by_start_dateas super_user-only, and an operations allowlist delegates it only from 5.3.0-beta.3, where its authorization entry is first named (harperutility/operation_authorization.ts: unnamed atv5.2.9:373, named atv5.3.2:433).canListJobsallows a super user, and allows an allowlist that lists the operation only where the role editor'sgateVerdictsays the listing grants it for the instance's version. Any other role sees an explanation, and the query stays disabled, so no request is sent.range=30dlink reads as 7 days there), renders at most the 200 newest rows, and says how many it held back.hdb_jobis in harper'sNON_REPLICATING_SYSTEM_TABLES, so a cluster route shows the jobs of whichever node its request reached, and the copy says so.CSV imports show their job in a toast.
waitForJobreports every polled job through a newonUpdate, and the import shows a loading toast —Queued · 2s · job <id>, thenRunning · 14s · job <id>— that turns into the result when the load finishes. Harper writes nothing to a job between starting it and finishing it, so the toast shows state and elapsed time, not a percentage. The toast is settled fromuseImportDataMutation's ownonSuccess/onError, so it still settles if the modal unmounts mid-import, which the oldmutate(…, { onSuccess })toast did not. A throw from any listener callback — per poll, success or error — is caught and logged, so it cannot turn a load Harper finished into a reported failure.The change stays self-contained to avoid kylebernhardy's open Status work (#1456, #1458, #1459, #1460, #1485, #1486, #1516, #1517): new files under
status/jobs/, plus one tab registration and a shared tab predicate inStatusTabs.tsxandstatusSearch.ts.⚖️ Alternatives
The planning review returned
better-alternative-exists, and the graded leg kept three of its points as majors through round 2. Gemini ruled all three of those overrulings factually right. Here is how each point was resolved:hdb_jobrow only inexecuteJob(harperserver/serverHelpers/serverUtilities.ts), once per job-type operations call, and each job starts its own worker thread (jobRunner.tslaunchJobThread). So the graded leg's "100 jobs a minute" history is not a workload Harper sustains. The 7-day cap and the 200-row render cap bound the rest.RUNNINGinstance (central-managersrc/resources/cluster/Cluster.jshandleProxiedOperation), so the answering node is stable there. The node-scope copy covers the rest.StatusTimeControlsfromTimeRangePicker. Custom time range and brush/zoom for Status charts #1456 (custom time range and brush/zoom) is about to reshape that component. Jobs uses the sameTIME_PRESETS/REFRESH_OPTIONSand URL params in its own small toolbar instead.🔧 Changes
searchJobs.tsholds the request — window bounds, a 24h upper-bound allowance for server clock skew, the newest-first sort, refusing a non-list answer — and the query options.JobsTab.tsxholds the tab, the toolbar (windows up to 7 days), the table and the expandable details. A click on a row toggles its details, unless the click ends a text selection inside that row, so the user, type or message can be selected and copied.canListJobs.tsis the permission gate, andjobFormat.tsholds the status badge, duration and time helpers.StatusTabs.tsxregisters the tab and mounts it, applying the predicate to the picker and hint as well.statusSearch.tsadmitsjobsin the URL.importData.tsgains theImportProgressListenerand passes each poll to it, keepingstage'serrorTextfor a structured message on the same lines.importProgressToast.tsis the toast, andImportDataModal.tsxwires it in and drops its own success toast.statusSearch.tsaddsjobsRefresh, off by default, beside the charts'refresh.JobsTab.tsxpolls on it and writes a picked interval to it,StatusTabs.tsxcarries it through each navigation, and the query skips refocus and reconnect refetches while it is off. The docs' Jobs section says why.formatDuration.tslives insrc/lib, so the databases feature does not import status internals (Lint rule: forbid importing other features' internals #1496).docs/status-redesign.mdgets a Jobs section, including the grant rule, and an eighth tab, and the rootDESIGN.mdindex line is updated.✅ Verification
searchJobs.test.ts,canListJobs.test.ts(now version-driven: 5.3.0-beta.3 and 5.3.2 allow a listed grant, 5.2.9 and 5.3.0-beta.2 deny it, a loading version waits, an unreadable one is tried),jobFormat.test.ts,JobsTab.test.tsx(a 5.3 allowlist role gets the list, a 5.2 one gets the note with no jobs request, a failed version read is tried),StatusTabs.capability-gating.test.tsx(now 8 tabs, plus two Jobs deep-link cases),getJob.test.ts(the observer sees an ERROR job's structured message as sent and the rejection reads it as text, so both PRs' behaviors hold together),importData.test.tsx(both CSV sources, and the settle-after-unmount case),importProgressToast.test.ts,ImportDataModal.progress.test.tsxandformatDuration.test.ts.try/catch, the listener isolation, and the uploaded-CSV progress wiring. That last one had no test until this port: it stayed green when reverted, soimportData.test.tsxnow covers it. The 17 guards checked before the port are unchanged.34adaa96):JobsTab.test.tsxopens Jobs with no interval and sees one request across ten minutes, a refocus and a reconnect. It also shows that Jobs ignores a chartrefresh=30000, polls onjobsRefresh=30000, and writes a picked interval tojobsRefreshwithout touchingrefresh.StatusTabs.url-sync.test.tsxpins that a switch from a chart tab opens Jobs at off, and that tab, range and interval changes carry both values. The redirect test covers the fourth navigation.statusSearch.test.tsandstatusSearchScope.test.tspin the defaults, that the two values are read independently, and that an explicit Jobs 60s stays in the URL.34adaa96): 13 of 13 went red. They are the Jobs default back to 60s, a refocus refetch and a reconnect refetch while off, the query and the toolbar each reading the charts' interval, the picker writingrefresh, the interval dropped from the query, the validator readingjobsRefreshfromrefresh, a tab switch inheriting the charts' interval, and each of the four StatusTabs navigations droppingjobsRefresh. No cross-model round ran on this commit: it changes a default and adds one URL parameter.34adaa96the husky pre-commit hook's fullvitest runpassed 470 files (4,464 tests),npx tsc -b,npx oxlint --format stylish .andnpx dprint checkexited 0, and commitlint passed. The same gate passed on the merge commit (466 files).server/jobs/jobs.ts(handleGetJob,handleGetJobsByStartDate,getJobsInDateRange),jobRunner.ts/jobProcess.ts(no mid-run progress; object messages),resources/databases.ts(NON_REPLICATING_SYSTEM_TABLES), andutility/operation_authorization.tsatrelease_4.1.0,release_4.7.0,v5.0.0,v5.2.9,v5.2.15,v5.3.0-beta.2/beta.3/v5.3.2. The shape is unchanged from 4.1, so no version gate is needed for the request itself.keepPreviousDataacross an instance switch, the analytics tick, two test comments). The adjudicator noted that the version-read-failure branch is rarely reachable, because the instance route's loader already requiresregistration_info. The comments and docs now say so: the branch covers a failed refetch, not a refusal. After that wording fix, one more full round ran on the amended head (a661b95a). It converged with nothing new against the port. Its open major is the unbounded poll again, with a Harper-side cost traced this time (see the auto-refresh decision above).no-receipt(the 1Password SSH agent refused theirgit fetch) and the domain adjudicator failedauth; the third, ate87b8ed1, ran codex, gemini and domain.e87b8ed1addresses it. The check is scoped to the row rather than to any page selection. The test fails with the old handler, and it also fails with gemini's page-widegetSelection().toString()check.🤖 Generated by Claude Code (Claude Opus); posted via @dawsontoth.
🤖 Generated with Claude Code
Related PRs: #1868 overlaps (merged; same waitForJob and importData lines, ported onto), #1884 overlaps (merged; canListJobs ported onto its gateVerdict), 12 others independent
Complexity: medium
Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse; rounds=5; full=2 @ 34adaa9
Review-Attention: study ~9m (decisions: cluster-jobs-scope, tick-on-jobs, window-clamp-client-side, failed-version-tries-grant, do-less-alternative; raised: open major) @ 34adaa9