feat(inbox): show the hunt working, not just spinning (#1526)

A pass runs for over two minutes and reports nothing until it lands. The
only sign of life was a spinner inside a button and a number that did not
move, so the honest reading was that nothing was happening.

A band under the header now names the mailboxes actually being read, keeps
a running count of omgång and hämtade as passes land, and offers Avbryt
without hunting for it in Settings.

The bar is deliberately indeterminate. There is no honest percentage
inside a pass, and inventing one to look finished is worse than admitting
the wait. Reduced motion holds the sweep still and dims it instead of
removing it, because the band still has to read as active.

The result line waits for the run to end rather than sitting under a live
one, so a press replaces itself in place.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-12 14:59:51 +02:00
committed by GitHub
parent 555a2a20ae
commit 89cfa82f2f
4 changed files with 68 additions and 4 deletions
+20
View File
@@ -704,3 +704,23 @@ input[type="number"] {
opacity: 1;
}
}
/* Indeterminate sweep for the receipt hunt.
A pass runs for over two minutes and reports nothing until it lands, so
there is no honest percentage to show inside one. This says "still working"
without inventing progress. Reduced motion holds it still and dims it
rather than removing it: the band still has to read as active. */
@keyframes hunt-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.hunt-sweep {
animation: hunt-sweep 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.hunt-sweep {
animation: none;
width: 100%;
opacity: 0.35;
}
}
@@ -49,6 +49,7 @@ import {
} from 'lucide-react'
import Link from 'next/link'
import { cn, formatCurrency, formatDate, formatDateLong } from '@/lib/utils'
import { QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import { GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks'
import EditKonteringDialog from '@/components/extensions/general/EditKonteringDialog'
import { WhatsAppMark } from '@/components/extensions/general/WhatsAppMark'
@@ -1136,9 +1137,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
{hunting ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
{huntProgress
? `Letar… ${huntProgress.fetched} hittade`
: 'Letar…'}
{t('hunt_stop')}
</>
) : (
<>
@@ -1168,7 +1167,46 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
</div>
</header>
{huntResult && (
{/* A pass takes over two minutes and reports nothing until it lands, so
a spinner alone leaves somebody watching a button. This says which
mailboxes are being read, what has been found so far, and keeps
moving while the pass is silent. The bar is deliberately
indeterminate: there is no honest percentage inside a pass, and a
fake one is worse than none. */}
{hunting && (
<div className="border-b bg-secondary/30">
<div className="h-0.5 overflow-hidden bg-border/40">
<div className="hunt-sweep h-full w-1/3 bg-foreground/40" />
</div>
<div className="px-4 py-2.5 flex items-center gap-3 flex-wrap text-xs">
<span className="flex items-center gap-2 min-w-0">
<Loader2 className="h-3.5 w-3.5 animate-spin shrink-0 text-muted-foreground" />
<span className="truncate">
{t('hunt_reading', {
mailboxes: mailConnections
.filter((c) => c.status === 'active')
.map((c) => c.emailAddress)
.join(', '),
})}
</span>
</span>
<span className="flex-1" />
{huntProgress && (
<span className="text-muted-foreground tabular-nums">
{t('hunt_progress', {
pass: huntProgress.passes,
found: huntProgress.fetched,
})}
</span>
)}
<button type="button" onClick={stopHunt} className={QUIET_LINK_CLASS}>
{t('hunt_stop')}
</button>
</div>
</div>
)}
{!hunting && huntResult && (
<div className="border-b px-4 py-2 text-xs flex items-center gap-2">
{huntResult.failed ? (
<span className="text-warning">
+3
View File
@@ -3049,6 +3049,9 @@
"mixed_currency_note": "Documents in different currencies cannot be summed into a single amount. Each document is still booked separately, against its matched bank transaction and the amount the bank actually settled in SEK."
},
"inbox_workspace": {
"hunt_stop": "Stop",
"hunt_reading": "Reading {mailboxes}",
"hunt_progress": "pass {pass} · {found} fetched",
"dialog_no_document": "No document",
"sources_one": "{count} source",
"sources_many": "{count} sources",
+3
View File
@@ -3049,6 +3049,9 @@
"mixed_currency_note": "Underlag i olika valutor kan inte summeras till ett belopp. Varje underlag bokförs ändå var för sig, mot sin matchade banktransaktion och det belopp banken faktiskt drog i SEK."
},
"inbox_workspace": {
"hunt_stop": "Avbryt",
"hunt_reading": "Läser {mailboxes}",
"hunt_progress": "omgång {pass} · {found} hämtade",
"dialog_no_document": "Ingen handling",
"sources_one": "{count} källa",
"sources_many": "{count} källor",