Let reviews see low-confidence suggestions and rebase preview applies

A low-confidence answer was discarded inside Classify, so Analyse showed
the row as unchanged instead of a reviewable suggestion; the fallback
decision moves to the import path, which keeps the merchant link and the
recorded confidence. ApplyPreview now rebases onto the current journal:
unrelated commits during a minutes-long paced run no longer invalidate
the review, only an edit to a selected transaction itself conflicts, and
applied changes are pruned so the rest stay appliable.
This commit is contained in:
Lars Nolden
2026-09-12 18:35:38 +02:00
parent 9092c5721d
commit 2373790be3
6 changed files with 160 additions and 28 deletions
+17
View File
@@ -82,6 +82,12 @@ func (a *App) importFacts(ctx context.Context, s State, facts []domain.Facts, in
p, e := classification.Rules(t.Facts, s.Data)
if a.settings.ClassifyOnImport {
p, e = a.classifier.Classify(ctx, t.Facts, s.Data, false)
// A low-confidence category is never auto-applied on import: the
// merchant link and provenance stay, and Analyse shows the model's
// suggestion for review instead.
if e == nil && p.Enrichment.Classification.Confidence == "low" {
p.Enrichment.CategoryID = domain.Fallback(t.Facts).CategoryID
}
}
if e == nil {
e = addProposal(&s.Data, p, t.Facts)
@@ -927,6 +933,13 @@ func syncBackoff(now time.Time, ops operational) time.Duration {
func (a *App) RunScheduler(ctx context.Context) {
timer := time.NewTimer(time.Minute)
defer timer.Stop()
// Prices keep their own clock: they come from a different provider, they are
// wanted even when no bank is connected, and a sync backoff must not delay
// them. The first run is shortly after start, so a fresh install or a
// restart does not leave a day's holdings unvalued waiting for the tick;
// after that it is daily, which is as often as a close changes.
prices := time.NewTimer(quoteStartup)
defer prices.Stop()
for {
force := false
select {
@@ -934,6 +947,10 @@ func (a *App) RunScheduler(ctx context.Context) {
return
case <-a.syncRequested:
force = true
case <-prices.C:
a.RefreshQuotes(ctx)
prices.Reset(quoteInterval)
continue
case <-timer.C:
}
a.mu.Lock()