chore: release v0.26.2
This commit is contained in:
@@ -47,7 +47,8 @@ import {
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
DownloadLoTWUsers, GetLoTWUsersStatus,
|
||||
GetScpStatus, SetScpEnabled, DownloadScp,
|
||||
DownloadULSCounties, ULSStatus, BackfillUSCounties,
|
||||
DownloadULSCounties, ULSStatus, BackfillUSCounties, BackfillRDA, RDADatabaseCount,
|
||||
GetCtyDatInfo, RefreshCtyDat, GetAwardReferenceMeta, UpdateAwardReferenceList,
|
||||
ComputeStationInfo,
|
||||
GetUIPref, SetUIPref,
|
||||
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas, GetFlexBandPower, SaveFlexBandPower,
|
||||
@@ -203,6 +204,7 @@ type SectionId =
|
||||
| 'database'
|
||||
| 'autostart'
|
||||
| 'uscounties'
|
||||
| 'databases'
|
||||
| 'awards'
|
||||
| 'cat'
|
||||
| 'ftx'
|
||||
@@ -320,6 +322,15 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
{ kind: 'item', label: t('sec.autostart'), id: 'autostart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Maintenance is about DATA rather than settings: nothing in here changes
|
||||
// how OpsLog behaves, it refreshes what it knows. That is why the reference
|
||||
// lists moved out of the Awards manager — an operator updating SOTA is not
|
||||
// editing an award, and had to open one to do it.
|
||||
kind: 'group', label: t('nav.maintenance'), icon: Database, defaultOpen: true, children: [
|
||||
{ kind: 'item', label: t('sec.databases'), id: 'databases' },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'group', label: t('nav.hardware'), icon: Server, defaultOpen: true, children: hardware,
|
||||
},
|
||||
@@ -335,6 +346,7 @@ const SECTION_KEY: Partial<Record<SectionId, string>> = {
|
||||
foldersync: 'sec.foldersync',
|
||||
webpublish: 'sec.webpublish',
|
||||
uscounties: 'sec.uscounties',
|
||||
databases: 'sec.databases',
|
||||
awards: 'sec.awards', cat: 'sec.cat', rotator: 'sec.rotator', winkeyer: 'sec.winkeyer', antenna: 'sec.antenna',
|
||||
antgenius: 'sec.antgenius', tunergenius: 'sec.tunergenius', pgxl: 'sec.pgxl', flex: 'sec.flex', audio: 'sec.audio', general: 'sec.general', email: 'sec.email',
|
||||
relayauto: 'sec.relayauto',
|
||||
@@ -628,6 +640,19 @@ function AutostartPanelComponent() {
|
||||
<Input className="h-8 font-mono text-xs" value={p.args} placeholder="optional command-line arguments"
|
||||
onChange={(e) => patch(p.id, { args: e.target.value })} />
|
||||
</div>
|
||||
{/* Only what OpsLog started is ever closed — a copy the operator
|
||||
opened themselves is theirs. The hint says so, because the
|
||||
difference is invisible from here. */}
|
||||
<label className="flex items-start gap-2 text-xs cursor-pointer">
|
||||
<Checkbox className="mt-0.5" checked={!!(p as any).close_on_exit}
|
||||
onCheckedChange={(c) => patch(p.id, { close_on_exit: !!c } as any)} />
|
||||
<span>
|
||||
Close it when OpsLog closes{' '}
|
||||
<span className="text-muted-foreground">
|
||||
(asks it to close, the way clicking its cross would — and only if OpsLog started it)
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{launchMsg[p.id] && <div className="text-xs text-muted-foreground pl-[86px]">{launchMsg[p.id]}</div>}
|
||||
</div>
|
||||
))}
|
||||
@@ -1720,6 +1745,45 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try { await DownloadULSCounties(); }
|
||||
catch (e: any) { setUlsBusy(false); setUlsProgress(null); setUlsMsg({ ok: false, text: String(e?.message ?? e) }); }
|
||||
};
|
||||
// cty.dat is not downloaded by OpsLog — it is shipped and reloaded from disk —
|
||||
// so the page reports it rather than offering a button it cannot honour.
|
||||
const [ctyInfo, setCtyInfo] = useState<{ entities: number; file_mod_time?: string }>({ entities: 0 });
|
||||
const [ctyBusy, setCtyBusy] = useState(false);
|
||||
useEffect(() => { GetCtyDatInfo().then((i) => setCtyInfo(i as any)).catch(() => {}); }, []);
|
||||
const refreshCty = async () => {
|
||||
setCtyBusy(true);
|
||||
try { const i: any = await RefreshCtyDat(); setCtyInfo(i); } catch { /* the line keeps its old count */ }
|
||||
finally { setCtyBusy(false); }
|
||||
};
|
||||
// Award reference lists, and which of them have an online updater.
|
||||
const [refMeta, setRefMeta] = useState<Array<{ code: string; count: number; updated_at?: string; can_update: boolean }>>([]);
|
||||
const [refBusy, setRefBusy] = useState('');
|
||||
const [refMsg, setRefMsg] = useState('');
|
||||
const reloadRefMeta = () => { GetAwardReferenceMeta().then((m) => setRefMeta((m ?? []) as any)).catch(() => {}); };
|
||||
useEffect(reloadRefMeta, []);
|
||||
const updateRefList = async (code: string) => {
|
||||
setRefBusy(code); setRefMsg('');
|
||||
try { const m: any = await UpdateAwardReferenceList(code); setRefMsg(t('db.refUpdated', { code, n: m?.count ?? 0 })); reloadRefMeta(); }
|
||||
catch (e: any) { setRefMsg(String(e?.message ?? e)); }
|
||||
finally { setRefBusy(''); }
|
||||
};
|
||||
// A date for a person: the day, not the timestamp the backend stores.
|
||||
const fmtDay = (v?: string) => (v ? String(v).slice(0, 10) : '—');
|
||||
const [rdaCount, setRdaCount] = useState(0);
|
||||
const [rdaBusy, setRdaBusy] = useState(false);
|
||||
const [rdaUseCurrent, setRdaUseCurrent] = useState(true);
|
||||
const [rdaMsg, setRdaMsg] = useState<string | null>(null);
|
||||
useEffect(() => { RDADatabaseCount().then((n) => setRdaCount(Number(n) || 0)).catch(() => {}); }, []);
|
||||
const runRDABackfill = async () => {
|
||||
setRdaBusy(true); setRdaMsg(null);
|
||||
try {
|
||||
const r: any = await BackfillRDA(rdaUseCurrent);
|
||||
setRdaMsg(t('rda.backfillDone', {
|
||||
d: r?.dated ?? 0, c: r?.current ?? 0, u: r?.unknown ?? 0, k: r?.skipped ?? 0, s: r?.scanned ?? 0,
|
||||
}));
|
||||
} catch (e: any) { setRdaMsg(String(e?.message ?? e)); }
|
||||
finally { setRdaBusy(false); }
|
||||
};
|
||||
const runBackfill = async () => {
|
||||
setBackfillBusy(true); setBackfillMsg(null);
|
||||
try { const r: any = await BackfillUSCounties(); setBackfillMsg(t('uscty.backfillDone', { c: r?.county ?? 0, g: r?.grid ?? 0, s: r?.scanned ?? 0 })); }
|
||||
@@ -6914,6 +6978,141 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
}
|
||||
|
||||
// One line per database: what it holds, when it was refreshed, and the button
|
||||
// that refreshes it. Every one of these already had an updater somewhere —
|
||||
// buried in the Awards manager, in a corner of the Lookup page, behind the
|
||||
// US Counties section — and an operator wanting them all current had to know
|
||||
// where each one lived. The point of this page is that they are in one place,
|
||||
// named, with their own line.
|
||||
function DatabasesPanel() {
|
||||
const rows: Array<{
|
||||
key: string; name: string; detail: string; busy: boolean; run?: () => void; note?: string;
|
||||
}> = [
|
||||
{
|
||||
key: 'cty', name: t('db.cty'),
|
||||
detail: ctyInfo?.entities ? t('db.ctyDetail', { n: ctyInfo.entities, d: fmtDay(ctyInfo.file_mod_time) }) : t('db.never'),
|
||||
busy: ctyBusy, run: refreshCty,
|
||||
},
|
||||
{
|
||||
key: 'clublog', name: t('db.clublog'),
|
||||
detail: clubInfo?.count ? t('db.clublogDetail', { n: clubInfo.count, d: clubInfo.date || '—' }) : t('db.never'),
|
||||
busy: clubBusy,
|
||||
run: () => { setClubBusy(true); setClubErr(''); DownloadClublogCty().then((i) => setClubInfo(i as ClubInfo)).catch((e: any) => setClubErr(String(e?.message ?? e))).finally(() => setClubBusy(false)); },
|
||||
},
|
||||
{
|
||||
key: 'lotw', name: t('db.lotwUsers'),
|
||||
detail: lotwUsers?.count ? t('db.lotwDetail', { n: lotwUsers.count, d: fmtDay(lotwUsers.updated) }) : t('db.never'),
|
||||
busy: lotwUsersBusy, run: downloadLotwUsers,
|
||||
},
|
||||
{
|
||||
key: 'scp', name: t('db.scp'),
|
||||
detail: scp?.count ? t('db.scpDetail', { n: scp.count, d: fmtDay(scp.updated) }) : t('db.never'),
|
||||
busy: scpBusy, run: downloadScp,
|
||||
},
|
||||
{
|
||||
key: 'uls', name: t('db.uls'),
|
||||
detail: ulsStatus?.count ? t('db.ulsDetail', { n: ulsStatus.count, d: fmtDay(ulsStatus.updated_at) }) : t('db.never'),
|
||||
busy: ulsBusy, run: downloadUls,
|
||||
},
|
||||
{
|
||||
key: 'rda', name: t('db.rda'),
|
||||
detail: t('db.rdaDetail', { n: rdaCount.toLocaleString() }),
|
||||
busy: false, note: t('db.rdaNote'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="space-y-4 max-w-3xl">
|
||||
<SectionHeader title={t('sec.databases')} hint={t('db.hint')} />
|
||||
<div className="rounded-md border border-border divide-y divide-border">
|
||||
{rows.map((r) => (
|
||||
<div key={r.key} className="flex items-center gap-3 p-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{r.name}</div>
|
||||
<div className="text-[11px] text-muted-foreground">{r.detail}</div>
|
||||
</div>
|
||||
{r.run ? (
|
||||
<Button size="sm" variant="secondary" onClick={r.run} disabled={r.busy}>
|
||||
{r.busy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('db.update')}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-[11px] text-muted-foreground italic shrink-0">{r.note}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* The award reference lists. Their own block because they are one kind
|
||||
of thing with one updater each, and because the list depends on which
|
||||
awards are defined — a shared WAPC brought in by an operator appears
|
||||
here the day it can be updated. */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium">{t('db.refLists')}</div>
|
||||
<p className="text-[11px] text-muted-foreground">{t('db.refListsHint')}</p>
|
||||
<div className="rounded-md border border-border divide-y divide-border">
|
||||
{refMeta.filter((m) => m.can_update).map((m) => (
|
||||
<div key={m.code} className="flex items-center gap-3 p-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium font-mono">{m.code}</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{m.count ? t('db.refDetail', { n: m.count, d: fmtDay(m.updated_at) }) : t('db.never')}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" disabled={refBusy === m.code}
|
||||
onClick={() => updateRefList(m.code)}>
|
||||
{refBusy === m.code ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('db.update')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{refMeta.filter((m) => m.can_update).length === 0 && (
|
||||
<div className="p-3 text-[11px] text-muted-foreground">{t('db.noRefLists')}</div>
|
||||
)}
|
||||
</div>
|
||||
{refMsg && <p className="text-[11px] text-muted-foreground">{refMsg}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Russian districts: the offline RDA database and the backfill it feeds.
|
||||
//
|
||||
// Its own section rather than a corner of the Awards manager: it is a
|
||||
// database and a bulk operation on the log, which is what every other item in
|
||||
// this part of the sidebar is.
|
||||
function RDAPanel() {
|
||||
return (
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<SectionHeader title={t('sec.rda')} hint={t('rda.hint')} />
|
||||
<div className="rounded-md border border-border p-3 space-y-1">
|
||||
<div className="text-xs font-medium">{t('rda.dbTitle')}</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t('rda.dbCount', { n: rdaCount.toLocaleString() })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-border p-3 space-y-2">
|
||||
<div className="text-xs font-medium">{t('rda.backfillTitle')}</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('rda.backfillIntro')}</p>
|
||||
{/* The choice is the whole point of the panel, so it is a checkbox and
|
||||
not a hidden default: a dated record is a fact, a current district
|
||||
on a ten-year-old QSO is an assumption. */}
|
||||
<label className="flex items-start gap-2 text-xs cursor-pointer">
|
||||
<Checkbox checked={rdaUseCurrent} className="mt-0.5" onCheckedChange={(c) => setRdaUseCurrent(!!c)} />
|
||||
<span>{t('rda.useCurrent')} <span className="text-muted-foreground">{t('rda.useCurrentHint')}</span></span>
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" variant="secondary" onClick={runRDABackfill} disabled={rdaBusy}>
|
||||
{rdaBusy ? <Loader2 className="size-3.5 animate-spin mr-1.5" /> : null}
|
||||
{t('rda.backfillRun')}
|
||||
</Button>
|
||||
{rdaMsg && <span className="text-xs text-muted-foreground">{rdaMsg}</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">{t('rda.neverOverwrites')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Map sections to their content + icon (for placeholder).
|
||||
const PANELS: Record<SectionId, () => JSX.Element> = {
|
||||
general: GeneralPanel,
|
||||
@@ -6940,8 +7139,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
backup: BackupPanel,
|
||||
database: DatabasePanel,
|
||||
uscounties: USCountiesPanel,
|
||||
databases: DatabasesPanel,
|
||||
autostart: () => <AutostartPanelComponent />,
|
||||
awards: () => <AwardsSelectionPanel profile={activeProfile ?? undefined} />,
|
||||
awards: () => (<div className="space-y-6"><AwardsSelectionPanel profile={activeProfile ?? undefined} /><RDAPanel /></div>),
|
||||
cat: CATPanel,
|
||||
rotator: RotatorPanel,
|
||||
winkeyer: WinkeyerPanel,
|
||||
|
||||
Reference in New Issue
Block a user