feat(jobs): surface background jobs with progress, logs and controls

Add a central job registry that library scans and search index builds
report into, so background work is visible instead of buried in the
settings page.

- backend/jobs: registry with per-job ring-buffer logs, capability-driven
  controls, and one coalesced JobsChanged snapshot at 4Hz
- pause survives restart via a job_state table; a paused scan is adopted
  back on launch and skipped by the soft scan
- top-bar indicator, popover, details drawer and a Jobs page replacing
  the config page's scan UI; per-library start/stop retained
- scan timing breakdown moves into the job log, Full rescan to the Jobs
  page; delete the orphaned library-manager component

Also add cmd/indexbuild and cmd/indexexport so the explore index can be
built once centrally rather than by every install, which today streams
~205GB from the ListenBrainz spark dump on first run. indexbuild picks
build/refresh/rebuild from index state; the Gitea workflow runs it on
push, weekly, or manually and publishes only when content changed.

fresh-install no longer defaults YJ_HOME under /tmp: it is tmpfs on most
distros, and the import needs ~6GB of real disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:42:22 -04:00
co-authored by Claude Opus 5
parent aead8eaef4
commit 01bc5f2094
48 changed files with 6656 additions and 2271 deletions
+47
View File
@@ -14,6 +14,7 @@ import (
"yellowjacket/backend/database"
"yellowjacket/backend/events"
"yellowjacket/backend/jobs"
)
// Service is the Wails-bound service for the explore feature.
@@ -148,6 +149,52 @@ func (e *Service) StopIndexBuild() {
e.index.StopBuild()
}
// SetJobRegistry wires the background job registry into the search
// index so its build reports progress and controls to the frontend.
func (e *Service) SetJobRegistry(reg *jobs.Registry) {
e.index.SetJobRegistry(reg)
}
// AdoptPausedIndexBuild re-registers a build paused in a previous
// session so it appears in the jobs panel, still paused.
func (e *Service) AdoptPausedIndexBuild() {
e.index.AdoptPausedBuild()
}
// IndexImportComplete reports whether the dump import has finished all
// of its stages. Distinct from IsIndexReady, which only means the index
// holds enough rows to answer queries — a partially imported index is
// ready but not complete. Used by the headless builder to decide
// whether another run is needed.
func (e *Service) IndexImportComplete() bool {
return e.index.ImportComplete()
}
// IndexBaselineSeries returns the incremental listens series the index's
// popularity is caught up to. A change across a refresh means new data
// was folded in.
func (e *Service) IndexBaselineSeries() int {
return e.index.BaselineSeries()
}
// IndexLastImported returns when the dump import last completed, or the
// zero time if it never has.
func (e *Service) IndexLastImported() time.Time {
return e.index.LastImported()
}
// PrepareIndexRebuild clears the completion marker so the next build
// re-imports from the newest published dump.
func (e *Service) PrepareIndexRebuild() {
e.index.PrepareRebuild()
}
// RefreshIndexNow folds newly published incremental listens dumps into
// the index synchronously. Pass 0 to bypass the cadence gate.
func (e *Service) RefreshIndexNow(minInterval time.Duration) {
e.index.RefreshNow(e.ctx, minInterval)
}
// IsIndexReady returns true once the index has been populated.
func (e *Service) IsIndexReady() bool {
return e.index.IsReady()