<bg-jobs>

bg-jobs.mjs · the queue underneath it: Job queue · the row reconciler it renders through: StableTable

An operator console for the background job queue: a nine-column table of every job the tab can see, a row of counts by status, a Retry all action, and a per-row delete. Use it on a page for inspecting jobs and resolving queue problems. It is not a progress indicator for one piece of work — for that, hold the {id, done} the enqueue call returned, or poll query_group(), and show a status indicator beside the feature that started the work.

Add the element without configuration

The element exposes no attributes, properties, slots, events, or initialization options that change its behavior. <bg-jobs></bg-jobs> after a <script type="module" src="/ui/components/bg-jobs.mjs"></script> is all the setup required — see Getting started for the page structure and Conventions for automatic registration on import.

The component cannot filter rows, choose columns, restrict itself to one group_id, hide destructive actions, or translate labels. If you need those features, use query_all() or query_group(), subscribe to events, and render a separate table. StableTable can provide the same row stability.

ElementAttributesEventsCSS custom properties
<bg-jobs> none none none — it styles itself from the shared tokens and the .badge / .btn classes

The view and its actions cover all jobs

Each row uses the job's optional label, falling back to its type. Suspended volatile work, and persistent work parked under a held lock (job queue § Yielding), shows paused in the row and summary; it resumes when the feature releases its suspension.

The table is filled from query_all(), which returns this tab's volatile jobs plus every persistent record currently in the database — active and historic, whichever tab created them, whichever session. There is no filter of any kind, so what the reader sees is the whole queue for the browser origin.

Both actions affect stored jobs directly. The per-row delete calls delete_job() on that row. It reaches a running job too: the run is stopped first and then the record goes, which is the only way to end a job whose executor will not finish. Retry all calls retry_all(), which resets and reprocesses every pending, paused and failed job in the database — not the rows on screen, not a selection, and not only the ones that arrived since the page loaded. For a persistent fetch job that means the stored request is sent again, so pressing it resends requests and may repeat writes.

Use this element on a dedicated operator or diagnostics page. Adding it beside an ordinary feature would let users replay failed requests from unrelated work in the same browser.

This page has no live demo because it shares the application’s queue. These docs are served from the same origin as the application around them and run on the same configured queue names, so a <bg-jobs> mounted here would list the reader's real jobs and its buttons would delete and replay those real jobs. The component has no option to isolate demo data. To try queue operations, use the demos on Job queue; to try the table’s row-update behaviour, use the demos on StableTable.

The component makes the page's main content full width

The component's own stylesheet carries a rule about its host page, not about itself:

body > main:has(bg-jobs) { max-width: none; }

Nine columns do not fit a reading column, so wherever the element mounts inside a page's <main>, that page goes full width. Three consequences are worth knowing before you place it:

A page that must keep its reading column therefore does not mount this element inside <main>. The selector is anchored at body > main, so a <main> nested deeper — inside a dialog, inside a demo — is not the one it widens; these docs pages have no top-level <main>, which is why nothing here changes width.

Row contents

Nine columns, fixed widths from a <colgroup> so the table does not re-measure as content changes:

ColumnShows
IDThe first 8 characters of the job's UUID, monospaced — enough to match a row against a log line, not the key itself
TypeThe job's type label, or empty when the caller supplied none
GroupThe first 8 characters of group_id, or an em dash when the job is ungrouped
StatusA .badge carrying pending, running, paused, done, failed or cancelled
Attemptsattempts over max_attempts, defaulting to 0 and 5
ErrorThe last error, truncated to one line; the full text is the cell's title
Created / UpdatedTime then date, both formatted for the locale l10n currently reports; an em dash when the timestamp is absent
(unlabelled)The delete button, an icon-only .btn-icon with an aria-label

With no jobs at all the table is hidden and an empty-state line takes its place — see Status messages for the vocabulary that line belongs to.

Rows are grouped by job group

Jobs are sorted newest-first by created_at and then re-grouped: every job sharing a group_id is emitted as one contiguous block, groups are ordered by their newest job, and the ungrouped jobs come last however new they are. As a result, individual rows need not follow chronological order: an older job can appear near the top because a newer job belongs to the same group. This keeps each batch together. The Created column therefore does not imply a strict chronological order for the whole table.

The counts row

A total and one badge per status are recomputed on each render from the same job array as the table. The counts appear beside Retry all.

Rows stay put while you are pointing at them

A queue under load re-renders constantly, and reflow could move a delete button while the user is trying to click it. The element renders through StableTable, which is what prevents that: while the pointer is over the rows or focus is inside them, changes to the row set are deferred and column widths stay fixed. Cell contents can still update. Leaving the table applies the deferred changes.

A click inside the rows allows structural updates for one second. Deleting a job explicitly requests a row change. Apply it immediately so the deleted row disappears without requiring the user to move the pointer.

This element needs a notification surface on the page to be fully usable. While an update is held back, the table registers a short message and an action to apply the update in the notifications store — which is only visible if the page also mounts a <ui-statusbar> or a <ui-notifications>. Without one, the message has no visible display and the table simply looks frozen for as long as the pointer rests on it. Mount a surface next to it; the store is shared, so any surface on the page will do.

Moving it between views

Safe to mount, unmount and re-mount — a tab switcher does exactly that — because each connect rebuilds the element from scratch: fresh markup, a fresh row reconciler, a fresh subscription, and a fresh query_all(). Disconnecting removes the subscription and destroys the reconciler, releasing its resources.

Preserve the generation guard if you reuse this pattern. A generation counter is bumped on both connect and disconnect, and a query_all() that resolves against an older generation is discarded. Without it, a snapshot requested before a disconnect could arrive after reconnection and restore jobs deleted while the update subscription was detached. Any component that starts an async query in connectedCallback can encounter this race; a boolean isConnected check cannot detect it because the element has already reconnected.

Labels are in English

The heading, the column names, the button and the empty-state line are written into the element's own template in English, and there is no attribute to override them — unlike <ui-statusbar>, whose accessible name is an observed attribute a page can set. Only the timestamps follow the active locale.

So an application that needs this table in another language cannot get there by translating an attribute; it either accepts English here, or renders its own table over the same queue. See Localization for how a page's own markup is translated, and why an element that builds its content in connectedCallback is outside that mechanism's reach.