fix: bulk update no longer holds the application hostage, and says what it does

Two faults in the progress bar shipped an hour ago.

It was a modal. A hundred QSOs is a minute; a contest log is two thousand and
ten minutes, and for all of it the operator could do nothing else — while their
radio is on. It is now a card in the corner: same bar, same count, same
callsign, none of the imprisonment.

And the menu said "Update from QRZ.com" while the code has always walked EVERY
configured provider, falling through to HamQTH when QRZ answers not-found. The
label is now "Update from the callsign databases", which is what happens.

Noted while reading that code, NOT fixed here because it trades against the
user's API quota and is their call: this path goes through the lookup cache, so
a second run on the same callsigns re-reads the cache rather than reaching the
provider. The cache comment already argues the opposite for deliberate clicks —
"a lookup the operator asked for by clicking is a deliberate act and must reach
the provider" — and a right-click on a selection is exactly that.
This commit is contained in:
2026-07-30 18:00:19 +02:00
parent c931d8a762
commit 45e3f3edb8
3 changed files with 32 additions and 31 deletions
+26 -27
View File
@@ -6394,34 +6394,33 @@ export default function App() {
</Dialog>
)}
{/* Deliberately NOT a modal. A contest log is thousands of QSOs and one
network round trip each, so this runs for ten minutes \u2014 a dialog would
hold the whole application hostage for the duration, and the operator
has a radio to work. It sits in the corner, above everything, and
stays out of the way. */}
{bulkProgress && (
<Dialog open>
<DialogContent className="max-w-sm px-6" hideClose>
<DialogHeader className="px-2">
<DialogTitle>{t('bulk.progressTitle')}</DialogTitle>
</DialogHeader>
<div className="px-2 pb-4 space-y-2">
{(() => {
const { processed, total, call } = bulkProgress;
const pct = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 0;
return (
<>
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
<div
className={cn('h-full bg-primary rounded-full transition-[width] duration-150', total === 0 && 'w-1/3 animate-pulse')}
style={total > 0 ? { width: `${pct}%` } : undefined}
/>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span className="font-mono">{call || '\u00a0'}</span>
<span>{total > 0 ? `${processed} / ${total}` : ''}</span>
</div>
</>
);
})()}
</div>
</DialogContent>
</Dialog>
<div className="fixed bottom-3 right-3 z-50 w-64 rounded-lg border border-border bg-card/95 backdrop-blur shadow-lg px-3 py-2 space-y-1.5">
<div className="text-xs font-medium">{t('bulk.progressTitle')}</div>
{(() => {
const { processed, total, call } = bulkProgress;
const pct = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 0;
return (
<>
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
<div
className={cn('h-full bg-primary rounded-full transition-[width] duration-150', total === 0 && 'w-1/3 animate-pulse')}
style={total > 0 ? { width: `${pct}%` } : undefined}
/>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
<span className="font-mono">{call || '\u00a0'}</span>
<span className="tabular-nums">{total > 0 ? `${processed} / ${total}` : ''}</span>
</div>
</>
);
})()}
</div>
)}
{importing && (