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:
+7
@@ -2,6 +2,11 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {explore} from '../models';
|
||||
import {context} from '../models';
|
||||
import {jobs} from '../models';
|
||||
|
||||
export function AdoptPausedIndexBuild():Promise<void>;
|
||||
|
||||
export function BackfillLibraryDiscographies():Promise<void>;
|
||||
|
||||
export function BackfillLibraryLyrics():Promise<void>;
|
||||
|
||||
@@ -81,6 +86,8 @@ export function SearchLyrics(arg1:string):Promise<Array<explore.LyricsResult>>;
|
||||
|
||||
export function SetContext(arg1:context.Context):Promise<void>;
|
||||
|
||||
export function SetJobRegistry(arg1:jobs.Registry):Promise<void>;
|
||||
|
||||
export function SimilarArtists(arg1:string):Promise<Array<explore.LBSimilarArtist>>;
|
||||
|
||||
export function StartIndexBuild():Promise<void>;
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AdoptPausedIndexBuild() {
|
||||
return window['go']['explore']['Service']['AdoptPausedIndexBuild']();
|
||||
}
|
||||
|
||||
export function BackfillLibraryDiscographies() {
|
||||
return window['go']['explore']['Service']['BackfillLibraryDiscographies']();
|
||||
}
|
||||
|
||||
export function BackfillLibraryLyrics() {
|
||||
return window['go']['explore']['Service']['BackfillLibraryLyrics']();
|
||||
}
|
||||
@@ -158,6 +166,10 @@ export function SetContext(arg1) {
|
||||
return window['go']['explore']['Service']['SetContext'](arg1);
|
||||
}
|
||||
|
||||
export function SetJobRegistry(arg1) {
|
||||
return window['go']['explore']['Service']['SetJobRegistry'](arg1);
|
||||
}
|
||||
|
||||
export function SimilarArtists(arg1) {
|
||||
return window['go']['explore']['Service']['SimilarArtists'](arg1);
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {jobs} from '../models';
|
||||
|
||||
export function CancelJob(arg1:string):Promise<void>;
|
||||
|
||||
export function ClearFinishedJobs():Promise<void>;
|
||||
|
||||
export function DismissJob(arg1:string):Promise<void>;
|
||||
|
||||
export function GetJobLog(arg1:string):Promise<Array<jobs.LogEntry>>;
|
||||
|
||||
export function GetJobs():Promise<Array<jobs.Job>>;
|
||||
|
||||
export function PauseJob(arg1:string):Promise<void>;
|
||||
|
||||
export function ResumeJob(arg1:string):Promise<void>;
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CancelJob(arg1) {
|
||||
return window['go']['jobs']['Service']['CancelJob'](arg1);
|
||||
}
|
||||
|
||||
export function ClearFinishedJobs() {
|
||||
return window['go']['jobs']['Service']['ClearFinishedJobs']();
|
||||
}
|
||||
|
||||
export function DismissJob(arg1) {
|
||||
return window['go']['jobs']['Service']['DismissJob'](arg1);
|
||||
}
|
||||
|
||||
export function GetJobLog(arg1) {
|
||||
return window['go']['jobs']['Service']['GetJobLog'](arg1);
|
||||
}
|
||||
|
||||
export function GetJobs() {
|
||||
return window['go']['jobs']['Service']['GetJobs']();
|
||||
}
|
||||
|
||||
export function PauseJob(arg1) {
|
||||
return window['go']['jobs']['Service']['PauseJob'](arg1);
|
||||
}
|
||||
|
||||
export function ResumeJob(arg1) {
|
||||
return window['go']['jobs']['Service']['ResumeJob'](arg1);
|
||||
}
|
||||
+5
@@ -3,6 +3,7 @@
|
||||
import {sqlcgen} from '../models';
|
||||
import {library} from '../models';
|
||||
import {context} from '../models';
|
||||
import {jobs} from '../models';
|
||||
|
||||
export function AcquirePipelineLock():Promise<void>;
|
||||
|
||||
@@ -66,6 +67,8 @@ export function RemoveLibrary(arg1:number):Promise<library.RemovalSummary>;
|
||||
|
||||
export function RenameLibrary(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function RestorePausedScans():Promise<void>;
|
||||
|
||||
export function ResumeScan():Promise<void>;
|
||||
|
||||
export function ScanAllLibraries():Promise<void>;
|
||||
@@ -78,6 +81,8 @@ export function SearchTracksByLibrary(arg1:string,arg2:number):Promise<Array<lib
|
||||
|
||||
export function SetContext(arg1:context.Context):Promise<void>;
|
||||
|
||||
export function SetJobRegistry(arg1:jobs.Registry):Promise<void>;
|
||||
|
||||
export function SetRemovalHooks(arg1:library.RemovalHooks):Promise<void>;
|
||||
|
||||
export function SetRescanHooks(arg1:library.RescanHooks):Promise<void>;
|
||||
|
||||
@@ -126,6 +126,10 @@ export function RenameLibrary(arg1, arg2) {
|
||||
return window['go']['library']['Library']['RenameLibrary'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function RestorePausedScans() {
|
||||
return window['go']['library']['Library']['RestorePausedScans']();
|
||||
}
|
||||
|
||||
export function ResumeScan() {
|
||||
return window['go']['library']['Library']['ResumeScan']();
|
||||
}
|
||||
@@ -150,6 +154,10 @@ export function SetContext(arg1) {
|
||||
return window['go']['library']['Library']['SetContext'](arg1);
|
||||
}
|
||||
|
||||
export function SetJobRegistry(arg1) {
|
||||
return window['go']['library']['Library']['SetJobRegistry'](arg1);
|
||||
}
|
||||
|
||||
export function SetRemovalHooks(arg1) {
|
||||
return window['go']['library']['Library']['SetRemovalHooks'](arg1);
|
||||
}
|
||||
|
||||
@@ -777,6 +777,154 @@ export namespace explore {
|
||||
|
||||
}
|
||||
|
||||
export namespace jobs {
|
||||
|
||||
export class Caps {
|
||||
pausable: boolean;
|
||||
cancellable: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Caps(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pausable = source["pausable"];
|
||||
this.cancellable = source["cancellable"];
|
||||
}
|
||||
}
|
||||
export class Stat {
|
||||
label: string;
|
||||
value: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Stat(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.label = source["label"];
|
||||
this.value = source["value"];
|
||||
}
|
||||
}
|
||||
export class Stage {
|
||||
name: string;
|
||||
state: string;
|
||||
current: number;
|
||||
total: number;
|
||||
error?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Stage(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.state = source["state"];
|
||||
this.current = source["current"];
|
||||
this.total = source["total"];
|
||||
this.error = source["error"];
|
||||
}
|
||||
}
|
||||
export class Job {
|
||||
id: string;
|
||||
kind: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
state: string;
|
||||
phase?: string;
|
||||
current: number;
|
||||
total: number;
|
||||
caps: Caps;
|
||||
stages: Stage[];
|
||||
stats: Stat[];
|
||||
error?: string;
|
||||
startedAt: number;
|
||||
updatedAt: number;
|
||||
endedAt?: number;
|
||||
logCount: number;
|
||||
warnCount: number;
|
||||
errorCount: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Job(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.kind = source["kind"];
|
||||
this.title = source["title"];
|
||||
this.subtitle = source["subtitle"];
|
||||
this.state = source["state"];
|
||||
this.phase = source["phase"];
|
||||
this.current = source["current"];
|
||||
this.total = source["total"];
|
||||
this.caps = this.convertValues(source["caps"], Caps);
|
||||
this.stages = this.convertValues(source["stages"], Stage);
|
||||
this.stats = this.convertValues(source["stats"], Stat);
|
||||
this.error = source["error"];
|
||||
this.startedAt = source["startedAt"];
|
||||
this.updatedAt = source["updatedAt"];
|
||||
this.endedAt = source["endedAt"];
|
||||
this.logCount = source["logCount"];
|
||||
this.warnCount = source["warnCount"];
|
||||
this.errorCount = source["errorCount"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class LogEntry {
|
||||
time: number;
|
||||
level: string;
|
||||
message: string;
|
||||
detail?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new LogEntry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.time = source["time"];
|
||||
this.level = source["level"];
|
||||
this.message = source["message"];
|
||||
this.detail = source["detail"];
|
||||
}
|
||||
}
|
||||
export class Registry {
|
||||
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Registry(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export namespace library {
|
||||
|
||||
export class Album {
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ export function ImportPlaylist(arg1:string):Promise<playlist.Summary>;
|
||||
|
||||
export function ImportPlaylists(arg1:Array<string>):Promise<Array<playlist.Summary>>;
|
||||
|
||||
export function MaterializeUnmaterializedSmartPlaylists():Promise<void>;
|
||||
|
||||
export function PreviewSmartPlaylist(arg1:string):Promise<Array<library.Track>>;
|
||||
|
||||
export function RefreshSmartPlaylist(arg1:number):Promise<void>;
|
||||
|
||||
@@ -82,6 +82,10 @@ export function ImportPlaylists(arg1) {
|
||||
return window['go']['playlist']['Service']['ImportPlaylists'](arg1);
|
||||
}
|
||||
|
||||
export function MaterializeUnmaterializedSmartPlaylists() {
|
||||
return window['go']['playlist']['Service']['MaterializeUnmaterializedSmartPlaylists']();
|
||||
}
|
||||
|
||||
export function PreviewSmartPlaylist(arg1) {
|
||||
return window['go']['playlist']['Service']['PreviewSmartPlaylist'](arg1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user