fix: Added LOTW badge for Lotw users colored depending on their last upload
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
ListClusterServers, ClusterSpotStatuses, SendClusterSpot,
|
||||
GetCATSettings,
|
||||
GetSolarData,
|
||||
LoTWUserInfo,
|
||||
OperatingDefaultForBand,
|
||||
LogUDPLoggedADIF,
|
||||
ListCountries,
|
||||
@@ -383,6 +384,19 @@ export default function App() {
|
||||
const id = window.setInterval(load, 60 * 60 * 1000); // hourly fallback; the event refreshes it live
|
||||
return () => { off(); window.clearInterval(id); };
|
||||
}, []);
|
||||
// LoTW-user lookup for the entered callsign (from the cached ARRL activity list),
|
||||
// debounced. Drives a colour-coded LoTW badge by last-upload recency.
|
||||
const [lotwInfo, setLotwInfo] = useState<{ is_user: boolean; last_upload?: string; days_ago: number } | null>(null);
|
||||
useEffect(() => {
|
||||
const c = callsign.trim();
|
||||
if (!c) { setLotwInfo(null); return; }
|
||||
const run = () => { LoTWUserInfo(c).then((r) => setLotwInfo(r as any)).catch(() => setLotwInfo(null)); };
|
||||
const t = window.setTimeout(run, 300);
|
||||
// Re-run when the background auto-download finishes (so a call typed before
|
||||
// the list loaded gets its badge without re-typing).
|
||||
const off = EventsOn('lotwusers:updated', run);
|
||||
return () => { window.clearTimeout(t); off(); };
|
||||
}, [callsign]);
|
||||
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
|
||||
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
|
||||
const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] });
|
||||
@@ -2743,6 +2757,23 @@ export default function App() {
|
||||
<Input value={grid} placeholder="JN05" className="font-mono" onChange={(e) => { setGrid(e.target.value); markEdited('grid'); }} />
|
||||
</div>
|
||||
);
|
||||
// LoTW-user badge: shown only when the entered call is a LoTW user, coloured by
|
||||
// last-upload recency (green < 1 week · amber 1–4 weeks · red > 30 days). The
|
||||
// tooltip shows the exact last-upload date. Needs the list downloaded once
|
||||
// (Settings → LoTW); until then no badge appears.
|
||||
const lotwBlock = (lotwInfo && lotwInfo.is_user) ? (() => {
|
||||
const d = lotwInfo.days_ago ?? -1;
|
||||
const cls = d >= 0 && d < 7 ? 'border-success text-success bg-success/15'
|
||||
: d < 30 ? 'border-warning text-warning bg-warning/15'
|
||||
: 'border-danger text-danger bg-danger/15';
|
||||
return (
|
||||
<div className="self-end shrink-0" title={t('lotw.userTip', { date: lotwInfo.last_upload || '?', days: d })}>
|
||||
<span className={cn('inline-flex items-center rounded-md border px-1.5 py-1.5 text-[10px] font-extrabold tracking-wide leading-none', cls)}>
|
||||
LoTW
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})() : null;
|
||||
// A discreet spot-alert LED (bell) that fills the gap at the right of the Grid
|
||||
// row instead of the intrusive floating cards. It lights + shows a count when
|
||||
// alerts are pending; click it to see the last 3 (each clickable to tune).
|
||||
@@ -3706,6 +3737,7 @@ export default function App() {
|
||||
</div>
|
||||
{qthBlock}
|
||||
{gridBlock}
|
||||
{lotwBlock}
|
||||
{alertLedBlock}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
|
||||
GetPOTAToken, SavePOTAToken,
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
DownloadLoTWUsers, GetLoTWUsersStatus,
|
||||
ComputeStationInfo,
|
||||
GetUIPref, SetUIPref,
|
||||
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
|
||||
@@ -996,6 +997,16 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [clublogTesting, setClublogTesting] = useState(false);
|
||||
const [lotwTest, setLotwTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [lotwTesting, setLotwTesting] = useState(false);
|
||||
// LoTW user-activity list (for the callsign badge): count + last refresh.
|
||||
const [lotwUsers, setLotwUsers] = useState<{ count: number; updated?: string }>({ count: 0 });
|
||||
const [lotwUsersBusy, setLotwUsersBusy] = useState(false);
|
||||
useEffect(() => { GetLoTWUsersStatus().then((s) => setLotwUsers(s as any)).catch(() => {}); }, []);
|
||||
const downloadLotwUsers = async () => {
|
||||
setLotwUsersBusy(true);
|
||||
try { await DownloadLoTWUsers(); const s = await GetLoTWUsersStatus(); setLotwUsers(s as any); }
|
||||
catch (e: any) { setLotwTest({ ok: false, msg: String(e?.message ?? e) }); }
|
||||
finally { setLotwUsersBusy(false); }
|
||||
};
|
||||
const [hrdlogTest, setHrdlogTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
const [hrdlogTesting, setHrdlogTesting] = useState(false);
|
||||
const [eqslTest, setEqslTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
@@ -3551,6 +3562,23 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LoTW user list — powers the colour-coded "LoTW" badge next to the
|
||||
entered callsign (green < 1 week · amber 1–4 weeks · red > 30 days). */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<Label className="text-sm font-medium">{t('lotw.usersTitle')}</Label>
|
||||
<p className="text-[11px] text-muted-foreground">{t('lotw.usersHint')}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" onClick={downloadLotwUsers} disabled={lotwUsersBusy}>
|
||||
<ArrowDown className="size-3.5" /> {lotwUsersBusy ? t('lotw.usersDownloading') : t('lotw.usersDownload')}
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{lotwUsers.count > 0
|
||||
? t('lotw.usersLoaded', { n: lotwUsers.count.toLocaleString(), date: lotwUsers.updated ? new Date(lotwUsers.updated).toLocaleDateString() : '?' })
|
||||
: t('lotw.usersNone')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : extSvcTab === 'pota' ? (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
|
||||
@@ -14,6 +14,7 @@ type Dict = Record<string, string>;
|
||||
const en: Dict = {
|
||||
// Menu bar
|
||||
'prop.title': 'Propagation', 'prop.geomag': 'Geomag', 'prop.refresh': 'Refresh space weather',
|
||||
'lotw.userTip': 'LoTW user — last upload {date} ({days} days ago)',
|
||||
'menu.file': 'File', 'menu.edit': 'Edit', 'menu.view': 'View', 'menu.tools': 'Tools',
|
||||
'file.import': 'Import ADIF…', 'file.export': 'Export ADIF…', 'file.exporting': 'Exporting…',
|
||||
'file.exportCabrillo': 'Export Cabrillo…', 'file.deleteAll': 'Delete all QSOs…', 'file.exit': 'Exit',
|
||||
@@ -147,6 +148,7 @@ const en: Dict = {
|
||||
'cat.ubOk': 'Connected — the Ultrabeam responded with a status frame.',
|
||||
// External services (repeated labels)
|
||||
'es.autoUpload': 'Automatic upload on new QSO', 'es.uploadTiming': 'Upload timing', 'es.immediate': 'Immediate', 'es.delayed': 'Delayed (1–2 min, lets you fix mistakes)', 'es.onClose': 'On app close (batch)', 'es.testConn': 'Test connection', 'es.testing': 'Testing…', 'es.password': 'Password', 'es.apiKey': 'API key', 'es.forceCall': 'Force station callsign', 'es.accountEmail': 'Account email', 'es.logbookCall': 'Logbook callsign',
|
||||
'lotw.usersTitle': 'LoTW user list', 'lotw.usersHint': "Downloads ARRL's public list of LoTW users + their last-upload date, to show a colour-coded LoTW badge next to a callsign (green < 1 week · amber 1–4 weeks · red > 30 days).", 'lotw.usersDownload': 'Download LoTW user list', 'lotw.usersDownloading': 'Downloading…', 'lotw.usersLoaded': '{n} users loaded · updated {date}', 'lotw.usersNone': 'Not downloaded yet — the badge stays hidden until you do.',
|
||||
'hw.connecting': 'Connecting…', 'hw.testConn': 'Test connection', 'hw.sending': 'Sending…', 'hw.testRotator': 'Test (point to 0°)',
|
||||
// Profiles panel
|
||||
'prof.deleteConfirm': 'Delete profile "{name}"? All its settings will be lost.', 'prof.dupPrompt': 'Name for the new profile (copy of "{name}"):', 'prof.dupSuffix': '{name} Copy', 'prof.newPrompt': 'Name for the new profile:', 'prof.newDefault': 'New profile',
|
||||
@@ -225,6 +227,7 @@ const en: Dict = {
|
||||
|
||||
const fr: Dict = {
|
||||
'prop.title': 'Propagation', 'prop.geomag': 'Géomag', 'prop.refresh': 'Actualiser la météo spatiale',
|
||||
'lotw.userTip': 'Utilisateur LoTW — dernier upload {date} (il y a {days} j)',
|
||||
'menu.file': 'Fichier', 'menu.edit': 'Édition', 'menu.view': 'Affichage', 'menu.tools': 'Outils',
|
||||
'file.import': 'Importer ADIF…', 'file.export': 'Exporter ADIF…', 'file.exporting': 'Export…',
|
||||
'file.exportCabrillo': 'Exporter Cabrillo…', 'file.deleteAll': 'Supprimer tous les QSO…', 'file.exit': 'Quitter',
|
||||
@@ -342,6 +345,7 @@ const fr: Dict = {
|
||||
'cat.rotatorOk': "Paquet envoyé — l'antenne devrait tourner vers 0° (nord). Sinon, vérifie l'hôte/port PstRotator et que l'écouteur UDP de PstRotator est activé.",
|
||||
'cat.ubOk': "Connecté — l'Ultrabeam a répondu avec une trame de statut.",
|
||||
'es.autoUpload': 'Envoi automatique à chaque nouveau QSO', 'es.uploadTiming': "Moment de l'envoi", 'es.immediate': 'Immédiat', 'es.delayed': 'Différé (1–2 min, permet de corriger les erreurs)', 'es.onClose': "À la fermeture (par lot)", 'es.testConn': 'Tester la connexion', 'es.testing': 'Test…', 'es.password': 'Mot de passe', 'es.apiKey': 'Clé API', 'es.forceCall': "Forcer l'indicatif de station", 'es.accountEmail': 'E-mail du compte', 'es.logbookCall': 'Indicatif du carnet',
|
||||
'lotw.usersTitle': 'Liste des utilisateurs LoTW', 'lotw.usersHint': "Télécharge la liste publique ARRL des utilisateurs LoTW + leur date de dernier upload, pour afficher un badge LoTW coloré à côté d'un indicatif (vert < 1 semaine · ambre 1–4 semaines · rouge > 30 j).", 'lotw.usersDownload': 'Télécharger la liste LoTW', 'lotw.usersDownloading': 'Téléchargement…', 'lotw.usersLoaded': '{n} utilisateurs chargés · maj {date}', 'lotw.usersNone': 'Pas encore téléchargée — le badge reste caché tant que ce n’est pas fait.',
|
||||
'hw.connecting': 'Connexion…', 'hw.testConn': 'Tester la connexion', 'hw.sending': 'Envoi…', 'hw.testRotator': 'Tester (pointer vers 0°)',
|
||||
'prof.deleteConfirm': 'Supprimer le profil « {name} » ? Tous ses réglages seront perdus.', 'prof.dupPrompt': 'Nom du nouveau profil (copie de « {name} ») :', 'prof.dupSuffix': '{name} Copie', 'prof.newPrompt': 'Nom du nouveau profil :', 'prof.newDefault': 'Nouveau profil',
|
||||
'prof.hint': "Bascule entre tes identités d'opération (maison / portable / SOTA / contest). Choisis un profil ici, puis édite ses champs dans les autres sections (Informations station, etc.) — les changements sont enregistrés sur le profil sélectionné.", 'prof.active': 'ACTIF', 'prof.duplicate': 'Dupliquer', 'prof.delete': 'Supprimer', 'prof.profileName': 'Nom du profil',
|
||||
|
||||
Reference in New Issue
Block a user