Skip to content

add a Jobs view: pending runs and recorded jobs in one table (#2360, phase 4) - #2538

Open
ebuzerdrmz44 wants to merge 1 commit into
borgbase:masterfrom
ebuzerdrmz44:refactor/scheduler-jobs-view
Open

add a Jobs view: pending runs and recorded jobs in one table (#2360, phase 4)#2538
ebuzerdrmz44 wants to merge 1 commit into
borgbase:masterfrom
ebuzerdrmz44:refactor/scheduler-jobs-view

Conversation

@ebuzerdrmz44

Copy link
Copy Markdown
Contributor

Description

Adds the Jobs view from #2360 goal 3. One table showing what the scheduler is holding a time for, and what it has already recorded.

Depends on nothing in flight. Branches off master, and does not touch log_page.py.

Screenshot

Screenshot from 2026-08-12 14-40-26

Where it lives

A Jobs page in the Schedule tab's toolbox, following the Related Files map in the issue (views/schedule_tab.py - Add Jobs page), which also leaves the existing Log page alone.

The table is global, not scoped to the selected profile, so it has a Profile column and lists every profile's jobs. Flagging it because it was one of the issue's open questions and was never settled in words.

Where the rows come from

Recorded rows come from JobModel. Pending rows come from the scheduler's in-memory timers through a new VortaScheduler.pending_jobs(). So nothing writes Status.SCHEDULED to the table and the view has to merge the two halves itself.

pending_jobs() returns a PendingJob per profile whose timer is SCHEDULED or TOO_FAR_AHEAD, soonest first. It exists so the page never reads self.timers directly, which keeps the split in phase 2 an internal change.

Why the refresh is split in two

reload_records re-reads JobModel and is wired to backup_finished_event. reload_pending only walks the timers and is wired to schedule_changed.

They are separate because schedule_changed is a hot signal: reload_all_timers emits once per profile every 15 minutes, and three of the four emit sites fire while VortaScheduler.lock is held. A single refresh doing JobModel.select() would put a full table read inside the scheduler's critical section on every schedule change. The record query is also capped at 200 rows, since the 6 month purge is the only other bound on that table.

One residual worth naming: pending_jobs() still does a primary key profile lookup per armed timer inside that lock. That is a handful of rows rather than the whole history, so I left it rather than caching profile names in the scheduler.

What is deliberately not here

  • No completed or running rows. Nothing writes those statuses yet, so the table renders pending plus skipped and failed. The lifecycle transitions and the event_log link belong to the Execution phase, and the view picks them up for free once they land.
  • No filters. Next PR, as a combo box over the proxy. Status first, then profile. The Repository column is carried on the row for the same reason, so the repo filter has something to filter on.
  • No source role on the model, and no actions. Cancel and re-queue arrive with the actions PR, and the role that maps a proxy row back to a job goes in with its first real consumer rather than as an unused accessor here.

Notes

No schema change, no migration.

The Time column means created_at for a recorded job and the scheduled time for a pending one. That reads fine today because records only exist for skips, but once jobs carry real execution timestamps it is worth deciding whether the column is "when recorded" or "when due".

Tests cover the accessor (scheduled, too far ahead for a QTimer, profile deleted under its timer, never-run profile), the row rendering for both sources, and the page itself through a real schedule_changed emission.

Merging this allows me to go on Phase D.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant