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:
+4
-2
@@ -6,13 +6,15 @@
|
|||||||
"The Kenwood backend is now exercised against a rig that answers: OpsLog talks to the TS-2000 emulator it already carries for ACOM amplifiers, so frequency, mode, VFO, split and PTT are checked end to end. Testing on a real Kenwood is still needed, but the dialogue itself is no longer untried.",
|
"The Kenwood backend is now exercised against a rig that answers: OpsLog talks to the TS-2000 emulator it already carries for ACOM amplifiers, so frequency, mode, VFO, split and PTT are checked end to end. Testing on a real Kenwood is still needed, but the dialogue itself is no longer untried.",
|
||||||
"Kenwood: split is now detected from the transmit and receive VFO (FR/FT) when the radio leaves it out of its status frame — the case seen on a Flex in Kenwood CAT mode, where the frequency read correctly but split never appeared. Radios that report it in the status frame are unaffected.",
|
"Kenwood: split is now detected from the transmit and receive VFO (FR/FT) when the radio leaves it out of its status frame — the case seen on a Flex in Kenwood CAT mode, where the frequency read correctly but split never appeared. Radios that report it in the status frame are unaffected.",
|
||||||
"The CAT protocol trace covers the Kenwood backend as well, not just CI-V. Settings → CAT.",
|
"The CAT protocol trace covers the Kenwood backend as well, not just CI-V. Settings → CAT.",
|
||||||
"Updating selected QSOs from QRZ.com now shows a progress bar with the callsign being queried. On a contest log freshly imported, a hundred contacts mean a hundred network round trips, and until now nothing moved on screen while it worked."
|
"Updating selected QSOs from QRZ.com now shows a progress bar with the callsign being queried. On a contest log freshly imported, a hundred contacts mean a hundred network round trips, and until now nothing moved on screen while it worked.",
|
||||||
|
"That progress bar sits in a corner instead of a dialog, so the rest of OpsLog stays usable while thousands of QSOs are looked up. The menu entry is renamed \"Update from the callsign databases\": it has always queried every configured provider, QRZ.com then HamQTH, not QRZ.com alone."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Le backend Kenwood est désormais éprouvé face à une radio qui répond : OpsLog dialogue avec l'émulateur TS-2000 qu'il embarque déjà pour les amplis ACOM, ce qui vérifie fréquence, mode, VFO, split et PTT de bout en bout. Un essai sur un vrai Kenwood reste nécessaire, mais le dialogue n'est plus non testé.",
|
"Le backend Kenwood est désormais éprouvé face à une radio qui répond : OpsLog dialogue avec l'émulateur TS-2000 qu'il embarque déjà pour les amplis ACOM, ce qui vérifie fréquence, mode, VFO, split et PTT de bout en bout. Un essai sur un vrai Kenwood reste nécessaire, mais le dialogue n'est plus non testé.",
|
||||||
"Kenwood : le split est désormais détecté à partir des VFO d'émission et de réception (FR/FT) quand la radio ne le renseigne pas dans sa trame d'état — le cas observé sur un Flex en mode CAT Kenwood, où la fréquence était juste mais le split n'apparaissait jamais. Les radios qui le signalent normalement ne changent pas.",
|
"Kenwood : le split est désormais détecté à partir des VFO d'émission et de réception (FR/FT) quand la radio ne le renseigne pas dans sa trame d'état — le cas observé sur un Flex en mode CAT Kenwood, où la fréquence était juste mais le split n'apparaissait jamais. Les radios qui le signalent normalement ne changent pas.",
|
||||||
"La trace du protocole CAT couvre aussi le backend Kenwood, plus seulement le CI-V. Réglages → CAT.",
|
"La trace du protocole CAT couvre aussi le backend Kenwood, plus seulement le CI-V. Réglages → CAT.",
|
||||||
"La mise à jour des QSO sélectionnés depuis QRZ.com affiche désormais une barre de progression avec l'indicatif en cours d'interrogation. Sur un log de concours fraîchement importé, cent contacts font cent allers-retours réseau, et jusqu'ici rien ne bougeait à l'écran pendant ce temps."
|
"La mise à jour des QSO sélectionnés depuis QRZ.com affiche désormais une barre de progression avec l'indicatif en cours d'interrogation. Sur un log de concours fraîchement importé, cent contacts font cent allers-retours réseau, et jusqu'ici rien ne bougeait à l'écran pendant ce temps.",
|
||||||
|
"Cette barre de progression s'affiche dans un coin plutôt qu'en fenêtre : le reste d'OpsLog reste utilisable pendant l'interrogation de milliers de QSO. L'entrée de menu devient « Mettre à jour depuis les annuaires » : elle a toujours interrogé tous les annuaires configurés, QRZ.com puis HamQTH, et pas QRZ.com seul."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+26
-27
@@ -6394,34 +6394,33 @@ export default function App() {
|
|||||||
</Dialog>
|
</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 && (
|
{bulkProgress && (
|
||||||
<Dialog open>
|
<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">
|
||||||
<DialogContent className="max-w-sm px-6" hideClose>
|
<div className="text-xs font-medium">{t('bulk.progressTitle')}</div>
|
||||||
<DialogHeader className="px-2">
|
{(() => {
|
||||||
<DialogTitle>{t('bulk.progressTitle')}</DialogTitle>
|
const { processed, total, call } = bulkProgress;
|
||||||
</DialogHeader>
|
const pct = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 0;
|
||||||
<div className="px-2 pb-4 space-y-2">
|
return (
|
||||||
{(() => {
|
<>
|
||||||
const { processed, total, call } = bulkProgress;
|
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
|
||||||
const pct = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 0;
|
<div
|
||||||
return (
|
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 className="h-2 w-full rounded-full bg-muted overflow-hidden">
|
/>
|
||||||
<div
|
</div>
|
||||||
className={cn('h-full bg-primary rounded-full transition-[width] duration-150', total === 0 && 'w-1/3 animate-pulse')}
|
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||||
style={total > 0 ? { width: `${pct}%` } : undefined}
|
<span className="font-mono">{call || '\u00a0'}</span>
|
||||||
/>
|
<span className="tabular-nums">{total > 0 ? `${processed} / ${total}` : ''}</span>
|
||||||
</div>
|
</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>
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{importing && (
|
{importing && (
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ const en: Dict = {
|
|||||||
'awed.importReplace': 'Replace mine',
|
'awed.importReplace': 'Replace mine',
|
||||||
'awed.importCopy': 'Import as {code}-2',
|
'awed.importCopy': 'Import as {code}-2',
|
||||||
// QSO modals (context menu / bulk edit / QSL manager / QSO edit)
|
// QSO modals (context menu / bulk edit / QSL manager / QSO edit)
|
||||||
'qctx.selected': '{n} QSO(s) selected', 'qctx.fixCountry': 'Fix country & zones from cty.dat', 'qctx.updateQrz': 'Update from QRZ.com', 'qctx.updateClublog': 'Update from ClubLog (exceptions)', 'qctx.sendQslEmail': 'Send OpsLog QSL by e-mail', 'qctx.sendRecording': 'Send recording by e-mail', 'qctx.bulkEdit': 'Bulk edit field… ({n})', 'qctx.exportSelectedAdif': 'Export selected to ADIF ({n})', 'qctx.exportSelectedFields': 'Export selected — choose fields… ({n})', 'qctx.exportFilteredAdif': 'Export filtered view to ADIF (no limit)', 'qctx.exportSelectedCabrillo': 'Export selected to Cabrillo ({n})', 'qctx.exportFilteredCabrillo': 'Export filtered view to Cabrillo (no limit)', 'qctx.sendTo': 'Send to {name}', 'qctx.delete': 'Delete {n} QSO(s)…',
|
'qctx.selected': '{n} QSO(s) selected', 'qctx.fixCountry': 'Fix country & zones from cty.dat', 'qctx.updateQrz': 'Update from the callsign databases', 'qctx.updateClublog': 'Update from ClubLog (exceptions)', 'qctx.sendQslEmail': 'Send OpsLog QSL by e-mail', 'qctx.sendRecording': 'Send recording by e-mail', 'qctx.bulkEdit': 'Bulk edit field… ({n})', 'qctx.exportSelectedAdif': 'Export selected to ADIF ({n})', 'qctx.exportSelectedFields': 'Export selected — choose fields… ({n})', 'qctx.exportFilteredAdif': 'Export filtered view to ADIF (no limit)', 'qctx.exportSelectedCabrillo': 'Export selected to Cabrillo ({n})', 'qctx.exportFilteredCabrillo': 'Export filtered view to Cabrillo (no limit)', 'qctx.sendTo': 'Send to {name}', 'qctx.delete': 'Delete {n} QSO(s)…',
|
||||||
'exp.title': 'Export to ADIF', 'exp.desc': 'Choose which fields to write.',
|
'exp.title': 'Export to ADIF', 'exp.desc': 'Choose which fields to write.',
|
||||||
'exp.stdTitle': 'Standard ADIF fields', 'exp.stdDesc': 'Official ADIF 3.1.7 fields only — best for uploading to other logbooks (LoTW, QRZ, Club Log…).',
|
'exp.stdTitle': 'Standard ADIF fields', 'exp.stdDesc': 'Official ADIF 3.1.7 fields only — best for uploading to other logbooks (LoTW, QRZ, Club Log…).',
|
||||||
'exp.allTitle': 'All OpsLog fields', 'exp.allDesc': 'Everything, including OpsLog-specific and app-defined tags — a full backup you can re-import here.',
|
'exp.allTitle': 'All OpsLog fields', 'exp.allDesc': 'Everything, including OpsLog-specific and app-defined tags — a full backup you can re-import here.',
|
||||||
@@ -790,7 +790,7 @@ const fr: Dict = {
|
|||||||
'awed.importKeepMine': 'Garder le mien',
|
'awed.importKeepMine': 'Garder le mien',
|
||||||
'awed.importReplace': 'Remplacer le mien',
|
'awed.importReplace': 'Remplacer le mien',
|
||||||
'awed.importCopy': 'Importer en {code}-2',
|
'awed.importCopy': 'Importer en {code}-2',
|
||||||
'qctx.selected': '{n} QSO sélectionné(s)', 'qctx.fixCountry': 'Corriger pays et zones depuis cty.dat', 'qctx.updateQrz': 'Mettre à jour depuis QRZ.com', 'qctx.updateClublog': 'Mettre à jour depuis ClubLog (exceptions)', 'qctx.sendQslEmail': 'Envoyer la QSL OpsLog par e-mail', 'qctx.sendRecording': "Envoyer l'enregistrement par e-mail", 'qctx.bulkEdit': "Édition groupée d'un champ… ({n})", 'qctx.exportSelectedAdif': 'Exporter la sélection en ADIF ({n})', 'qctx.exportFilteredAdif': 'Exporter la vue filtrée en ADIF (sans limite)', 'qctx.exportSelectedCabrillo': 'Exporter la sélection en Cabrillo ({n})', 'qctx.exportFilteredCabrillo': 'Exporter la vue filtrée en Cabrillo (sans limite)', 'qctx.sendTo': 'Envoyer vers {name}', 'qctx.delete': 'Supprimer {n} QSO…',
|
'qctx.selected': '{n} QSO sélectionné(s)', 'qctx.fixCountry': 'Corriger pays et zones depuis cty.dat', 'qctx.updateQrz': 'Mettre à jour depuis les annuaires', 'qctx.updateClublog': 'Mettre à jour depuis ClubLog (exceptions)', 'qctx.sendQslEmail': 'Envoyer la QSL OpsLog par e-mail', 'qctx.sendRecording': "Envoyer l'enregistrement par e-mail", 'qctx.bulkEdit': "Édition groupée d'un champ… ({n})", 'qctx.exportSelectedAdif': 'Exporter la sélection en ADIF ({n})', 'qctx.exportFilteredAdif': 'Exporter la vue filtrée en ADIF (sans limite)', 'qctx.exportSelectedCabrillo': 'Exporter la sélection en Cabrillo ({n})', 'qctx.exportFilteredCabrillo': 'Exporter la vue filtrée en Cabrillo (sans limite)', 'qctx.sendTo': 'Envoyer vers {name}', 'qctx.delete': 'Supprimer {n} QSO…',
|
||||||
'exp.title': 'Exporter en ADIF', 'exp.desc': 'Choisissez les champs à écrire.',
|
'exp.title': 'Exporter en ADIF', 'exp.desc': 'Choisissez les champs à écrire.',
|
||||||
'exp.stdTitle': 'Champs ADIF standard', 'exp.stdDesc': 'Uniquement les champs officiels ADIF 3.1.7 — idéal pour l’envoi vers d’autres carnets (LoTW, QRZ, Club Log…).',
|
'exp.stdTitle': 'Champs ADIF standard', 'exp.stdDesc': 'Uniquement les champs officiels ADIF 3.1.7 — idéal pour l’envoi vers d’autres carnets (LoTW, QRZ, Club Log…).',
|
||||||
'exp.allTitle': 'Tous les champs OpsLog', 'exp.allDesc': 'Tout, y compris les champs spécifiques à OpsLog et les balises applicatives — une sauvegarde complète ré-importable ici.',
|
'exp.allTitle': 'Tous les champs OpsLog', 'exp.allDesc': 'Tout, y compris les champs spécifiques à OpsLog et les balises applicatives — une sauvegarde complète ré-importable ici.',
|
||||||
|
|||||||
Reference in New Issue
Block a user