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
co-authored by Jakob Wennberg Claude Opus 5
parent 555a2a20ae
commit 89cfa82f2f
4 changed files with 68 additions and 4 deletions
@@ -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">