Fold the Jobs tab into the places the work is started #147

Merged
logan merged 7 commits from feat/27-jobs-into-settings into main 2026-08-20 01:02:41 +00:00
Collaborator

The Jobs destination is gone, and everything it carried is now beside
the thing that starts the work.

Commit Issue
feat(jobs): show background work where the work is started #27
feat(settings): scan from Settings, and watch every job in place #27
refactor(shell): retire the Jobs destination #27
test: cover jobs in Settings, and unpick two shared selectors #27
docs: record where background jobs went, and two traps #27

Why this is four panels and not one Settings section

The report says "the background-jobs tab should become a panel inside
Settings"; the Direction says split by kind. Reading the app first
settled it, because four of the five kinds already had a home showing
their work
:

kind already shown by
index-build, catalog-enrich Settings → Search Index's tier list
download downloads-view's lifecycle states
autotag-apply autotag-view's per-album progress ring
library-scan nothing but the tab

So the tab's unique content was the per-library scan controls — which
its own comment says had been taken out of Settings to build it — and
the generic affordances none of those four have: pause, cancel,
Details, the log, and a finished job you can dismiss.
<job-panel kinds="…"> carries those to each of them. One "Background
jobs" section in Settings would have been the tab again under another
name, and a catch-all is what a per-kind split is trying not to need.

The header job-indicator is untouched, per the Direction: it is still
the one view of everything at once, from every page.

Three rules in the panel

  • The controls are applyJobControl, not a reimplementation. That
    is what keeps the index build's "you will discard hours of
    downloading" confirmation alive — it is keyed on KindIndexBuild
    inside the shared handler, and a host drawing its own buttons would
    have dropped it silently. Asserted both ways: cancelling a scan
    does not ask, and that is the same code path.
  • An idle panel is hidden, host margin included — an empty panel
    in four places is four pieces of furniture describing an absence.
  • No "Clear finished", because ClearFinishedJobs is global: a
    Clear under Libraries would discard the index build's history too. A
    finished row dismisses itself.

The one thing #25 did not make free

RetiredViews is a migration, and it is worth saying why there is one
so soon after arguing there would not be. A visibility entry is a
map key, and an unknown key is dropped on load — that is what made this
issue's ordering safe. A launch page is a value, and an unknown
one fails validation, which on the load path means the app refuses to
start for anyone who had Jobs selected. So ApplyDefaults treats a
retired name as a zero value, while an unknown-but-not-retired one
still errors: that is a typo, and saying so is the useful answer.

Also fixed while there: config-page's Launch Page <select> still
listed its ten options by hand, which #25 left behind. It is derived
from VIEW_META now — removing a destination is precisely the change
that leaves two hand-written copies disagreeing.

Two shared selectors had to give

config-page config-section .header resolves to two elements once a
section holds a job, because job-details-drawer carries that class
too — so settings-reach.spec.ts (which has used it since plan 007)
failed Playwright strict mode. It locates a disclosure by role and name
now. Filed in .planning/NOTES.md, because it cannot be reproduced by
opening Settings and looking: the drawer only exists once a job does.

Verification

  • make lint (3 configurations) — 0 issues
  • make test (3 configurations) — pass, +3 cases in
    backend/config/views_test.go
  • make ui-test945 pass, including 5 new in job-panel.test.ts
  • make e2e155 pass (chromium), including 4 new in
    jobs-in-settings.spec.ts
  • make bindings-check, make css-check, make skill-check
  • npx tsc --noEmit in frontend/ and e2e/
  • By hand against make dev-headless SEED=default: sidebar without
    Jobs; Scan All from Settings produces a scan row with stats, a
    working Details drawer and a dismiss; Full Rescan asks first; the
    Launch Page select no longer offers Jobs; the Autotag view's layout
    is unchanged while idle. Screenshots read, not just assertions.

One unrelated flake met on the way is filed as #146 rather than
attributed to this branch — TestYtDlpSearchSkipsUnparseableLines
failed once in a full make test and has not reproduced in ten runs
since, on a diff that touches neither that package nor anything it
imports.

Closes #27

The Jobs destination is gone, and everything it carried is now beside the thing that starts the work. | Commit | Issue | |---|---| | `feat(jobs): show background work where the work is started` | #27 | | `feat(settings): scan from Settings, and watch every job in place` | #27 | | `refactor(shell): retire the Jobs destination` | #27 | | `test: cover jobs in Settings, and unpick two shared selectors` | #27 | | `docs: record where background jobs went, and two traps` | #27 | ## Why this is four panels and not one Settings section The report says "the background-jobs tab should become a panel inside Settings"; the Direction says split by kind. Reading the app first settled it, because **four of the five kinds already had a home showing their work**: | kind | already shown by | |---|---| | `index-build`, `catalog-enrich` | Settings → Search Index's tier list | | `download` | `downloads-view`'s lifecycle states | | `autotag-apply` | `autotag-view`'s per-album progress ring | | `library-scan` | **nothing but the tab** | So the tab's unique content was the per-library scan controls — which its own comment says had been taken *out* of Settings to build it — and the **generic affordances** none of those four have: pause, cancel, Details, the log, and a finished job you can dismiss. `<job-panel kinds="…">` carries those to each of them. One "Background jobs" section in Settings would have been the tab again under another name, and a catch-all is what a per-kind split is trying not to need. The header `job-indicator` is untouched, per the Direction: it is still the one view of everything at once, from every page. ## Three rules in the panel - **The controls are `applyJobControl`**, not a reimplementation. That is what keeps the index build's "you will discard hours of downloading" confirmation alive — it is keyed on `KindIndexBuild` inside the shared handler, and a host drawing its own buttons would have dropped it silently. Asserted both ways: cancelling a *scan* does not ask, and that is the same code path. - **An idle panel is `hidden`**, host margin included — an empty panel in four places is four pieces of furniture describing an absence. - **No "Clear finished"**, because `ClearFinishedJobs` is global: a Clear under Libraries would discard the index build's history too. A finished row dismisses itself. ## The one thing #25 did not make free `RetiredViews` is a migration, and it is worth saying why there is one so soon after arguing there would not be. A **visibility entry** is a map key, and an unknown key is dropped on load — that is what made this issue's ordering safe. A **launch page** is a *value*, and an unknown one fails validation, which on the load path means the app refuses to start for anyone who had Jobs selected. So `ApplyDefaults` treats a retired name as a zero value, while an unknown-but-not-retired one still errors: that is a typo, and saying so is the useful answer. Also fixed while there: `config-page`'s Launch Page `<select>` still listed its ten options by hand, which #25 left behind. It is derived from `VIEW_META` now — removing a destination is precisely the change that leaves two hand-written copies disagreeing. ## Two shared selectors had to give `config-page config-section .header` resolves to **two** elements once a section holds a job, because `job-details-drawer` carries that class too — so `settings-reach.spec.ts` (which has used it since plan 007) failed Playwright strict mode. It locates a disclosure by role and name now. Filed in `.planning/NOTES.md`, because it cannot be reproduced by opening Settings and looking: the drawer only exists once a job does. ## Verification - `make lint` (3 configurations) — 0 issues - `make test` (3 configurations) — pass, +3 cases in `backend/config/views_test.go` - `make ui-test` — **945 pass**, including 5 new in `job-panel.test.ts` - `make e2e` — **155 pass** (chromium), including 4 new in `jobs-in-settings.spec.ts` - `make bindings-check`, `make css-check`, `make skill-check` - `npx tsc --noEmit` in `frontend/` and `e2e/` - By hand against `make dev-headless SEED=default`: sidebar without Jobs; Scan All from Settings produces a scan row with stats, a working Details drawer and a dismiss; Full Rescan asks first; the Launch Page select no longer offers Jobs; the Autotag view's layout is unchanged while idle. Screenshots read, not just assertions. One unrelated flake met on the way is filed as #146 rather than attributed to this branch — `TestYtDlpSearchSkipsUnparseableLines` failed once in a full `make test` and has not reproduced in ten runs since, on a diff that touches neither that package nor anything it imports. Closes #27
logan added 5 commits 2026-08-20 00:28:54 +00:00
Reading the app before moving anything turned up that four of the five
job kinds already have a home showing their work: Settings → Search
Index draws per-tier index progress, `downloads-view` draws every
download's lifecycle state, `autotag-view` draws its own apply ring,
and only `library-scan` had nowhere but the Jobs tab. What none of the
four had is the *generic* affordances — pause, cancel, Details, the
log, and a finished job you can dismiss.

So this is a panel embedded beside each of them rather than one
"Background jobs" section in Settings, which would have been the tab
again under another name.

Three rules in it. The controls are `applyJobControl`, not a
reimplementation, which is what keeps the index build's "you will
discard hours of downloading" confirmation alive across the move. A
panel with nothing to say is `hidden` rather than empty, host margin
included, because an idle panel in four places is four pieces of
furniture describing an absence. And there is no "Clear finished":
`ClearFinishedJobs` is global, so a Clear under Libraries would discard
the index build's history too.

`JobKind` also gains `download`, which the backend has had all along.
Scanning goes back where libraries are managed -- the Jobs tab's own
comment said the per-library controls had been taken out of Settings to
build it. "Scan All" and "Full Rescan" join "Add Library", "Scan now"
joins the per-library overflow menu beside Rename and Remove, and a
library being scanned says so where its track count goes.

Each surface then gets the job rows for its own kind: scans under
Libraries, index and enrichment under Search Index, downloads under the
download clients, and the autotag apply in the Autotag view -- where
stopping a run matters most, since applying rewrites tags on disk and
that view had no cancel at all.

The autotag panel shares the header's grid row through a wrapper rather
than taking a third row: it is display:none while nothing is applying,
which is nearly always, and a grid row would still spend the
container's gap on it.

The Launch Page select is derived from `VIEW_META` instead of listing
its ten options, since removing a destination is exactly the change
that leaves two hand-written copies disagreeing.
Nothing it carried is gone -- the two commits before this put all of it
somewhere the work is already being done.

A retired destination is the one shape #25's storage decision does not
make free. A visibility entry is a map key and an unknown key is
dropped on load; a launch page is a *value*, and an unknown one fails
validation -- which on the load path means the app refuses to start for
whoever had Jobs selected. `RetiredViews` is that list, read by
`ApplyDefaults`, which treats a retired name as a zero value. An
unknown-but-not-retired name still errors, because that is a typo and
saying so is the useful answer.
The spec worth having is not that the tab is gone -- that is one line
of a table -- but that nothing became unreachable when it went. #24
promises that no action is ever unreachable at any supported size, and
deleting a destination is exactly the change that quietly breaks it.

Two existing selectors had to give. `config-section .header` is
ambiguous the moment a section holds a job, because `job-details-drawer`
carries that class too -- so `settings-reach.spec.ts` locates a
disclosure by role and name instead. And `page-header`'s and
`offline-icons`'s view lists lose an entry each.

Closes #27
docs: record where background jobs went, and two traps
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m37s
CI / e2e (pull_request) Failing after 7m35s
99f355b2fc
The retired-destination note sits beside #25's storage paragraph
because it is the exception to it: an absent key is free, a value is
not.

`config-section .header` resolving to two elements is in NOTES because
it cannot be reproduced by opening Settings and looking -- the drawer
only exists once a job does.
logan added 1 commit 2026-08-20 00:44:09 +00:00
fix(e2e): the third spec that located a disclosure by class
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Canceled after 8s
CI / e2e (pull_request) Canceled after 0s
e4efec6f0c
`config-section .header` is ambiguous once a section holds a job, and
`failure-voice.spec.ts` was the one I did not grep for. It passed on
chromium and failed on webkit in the same CI run, which is the tell:
the two engines share one app, so the second one runs with a finished
scan the first one left behind.

The NOTES entry already says a class name is not a selector's contract;
this is the same fix, by role and name.
logan added 1 commit 2026-08-20 00:44:22 +00:00
docs: note that CI's two engines share one app
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m31s
CI / e2e (pull_request) Successful in 7m32s
f9ba9a87d7
Which is why a shared-selector fault can be green on chromium and red
on webkit in the same run, and how to reproduce it locally.
logan merged commit 6e7e349e63 into main 2026-08-20 01:02:41 +00:00
Sign in to join this conversation.