Run classification previews in the background with live progress

The preview endpoint held one HTTP request open while classifying
serially at three-second pacing, so any real range meant minutes of a
grayed-out button and per-row errors were invisible until the loop
ended. Analyse now starts a single background run against its own
snapshot; a progress endpoint reports analysed counts, proposed
changes and errors as they happen, and the page polls it with a
progress bar, pace-based estimate and a Stop button. Navigating away
no longer orphans the run: the page re-attaches to it on return.

A run that has produced no successful proposal and fails three times
in a row with the identical error stops early and reports that error,
so a wrong key or unsupported model surfaces in seconds instead of
repeating across the whole paced range.

Also normalize a null settings.private_names, which crashed the whole
UI on a workspace that had never saved preferences.
This commit is contained in:
Lars Nolden
2026-09-12 12:24:50 +02:00
parent 635c11be56
commit 9092c5721d
10 changed files with 449 additions and 91 deletions
+15
View File
@@ -200,6 +200,20 @@ export interface Preview {
unchanged: number;
errors: { id: string; error: string }[];
}
// PreviewProgress is the live state of a background classification run.
// Errors accumulate as they happen; preview is present only when done
// without a fatal error.
export interface PreviewProgress {
id: string;
total: number;
analysed: number;
changes: number;
unchanged: number;
errors: { id: string; error: string }[];
done: boolean;
error?: string;
preview?: Preview;
}
export interface ProposedCategory {
name: string;
parent?: string;
@@ -395,6 +409,7 @@ export function normalizeState(state: State): State {
else if (!Array.isArray(state.data[key]))
throw new Error(`The server state has invalid ${key}.`);
}
state.settings.private_names ??= [];
for (const tx of state.data.transactions) tx.enrichment.tag_ids ??= [];
for (const merchant of state.data.merchants) {
merchant.aliases ??= [];