feat: day batch — upload results surfaced as toasts (silent LoTW failures from the right-click send looked like nothing was sent); live selection count + Select all/Unselect all toggle in the grid toolbar; active filters bypass the row limit (all matches shown, 10k safety cap); NET Control: same right-click menu on worked-before + drag&drop roster<->on-air (drop on roster logs the QSO); compact mode restores previous window size/position; advanced-filter field renamed Station callsign; optional DXCC-style digital-mode grouping (Settings->General) for matrix badges + cluster colouring; changelog 0.20.9 entries (version NOT bumped)

This commit is contained in:
2026-07-22 15:38:08 +02:00
parent 5aac28f564
commit 4c75680689
10 changed files with 284 additions and 39 deletions
+37 -12
View File
@@ -116,6 +116,9 @@ type ModePreset = ModePresetForm;
type WB = WorkedBeforeView;
type CATState = Omit<catModels.RigState, 'convertValues'>;
// Safety cap on how many rows an ACTIVE filter may load (the normal row
// threshold is bypassed while filtering — see buildActiveFilter).
const FILTERED_LIMIT_CAP = 10000;
const DEFAULT_BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','2m','70cm','23cm'];
const DEFAULT_MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE'];
// Modes the QSO recorder captures (phone only). Mirrors recordableMode() in
@@ -1431,13 +1434,21 @@ export default function App() {
// The full filter sent to the backend = the builder conditions + the quick
// callsign search box (always ANDed) + the on-screen row threshold.
const buildActiveFilter = useCallback((): QueryFilter => ({
quick_callsign: filterCallsign,
conditions: activeFilter.conditions ?? [],
match: activeFilter.match ?? 'AND',
limit: qsoLimit,
offset: 0,
}), [filterCallsign, activeFilter, qsoLimit]);
// When a filter is ACTIVE the row threshold is lifted to a high safety cap:
// the threshold exists to keep the UNFILTERED 30k-QSO log fast, but applying
// it to a filter meant "200 matches, only the first 100 shown". A filtered
// result set is almost always small, and the cap protects against a
// broad filter (e.g. band=20m) pulling half the log.
const buildActiveFilter = useCallback((): QueryFilter => {
const hasFilter = !!(filterCallsign || (activeFilter.conditions ?? []).length);
return {
quick_callsign: filterCallsign,
conditions: activeFilter.conditions ?? [],
match: activeFilter.match ?? 'AND',
limit: hasFilter ? FILTERED_LIMIT_CAP : qsoLimit,
offset: 0,
};
}, [filterCallsign, activeFilter, qsoLimit]);
// refresh reloads the grid. Returns false on failure. When silent, it doesn't
// surface the error — used by the startup retry, since the logbook DB (a remote
@@ -3729,7 +3740,12 @@ export default function App() {
case 'netcontrol':
return (
<div className="h-full w-full min-h-0 flex flex-col rounded-lg overflow-hidden border border-border">
<NetControlPanel onLogged={refresh} countries={countries} bands={bands} modes={modes} />
<NetControlPanel onLogged={refresh} countries={countries} bands={bands} modes={modes}
qsoMenuHandlers={{
onUpdateFromCty: bulkUpdateFromCty, onUpdateFromQRZ: bulkUpdateFromQRZ, onUpdateFromClublog: bulkUpdateFromClublog,
onSendTo: bulkSendTo, onSendRecording: bulkSendRecording, onSendEQSL: (ids: number[]) => setEqslQsoId(ids[0] ?? null),
onDelete: (ids: number[]) => setDeletingIds(ids),
}} />
</div>
);
case 'recent':
@@ -4796,9 +4812,13 @@ export default function App() {
)}
</div>
<div className="flex items-center gap-2">
{qsos.length >= qsoLimit && qsos.length < total && (
<span className="text-warning">Limit reached raise Max to see more.</span>
)}
{(activeFilter.conditions?.length || filterCallsign)
? (matchCount != null && matchCount > FILTERED_LIMIT_CAP && (
<span className="text-warning">{matchCount.toLocaleString('en-US')} matches only {FILTERED_LIMIT_CAP.toLocaleString('en-US')} shown, narrow the filter (or use the filtered ADIF export).</span>
))
: (qsos.length >= qsoLimit && qsos.length < total && (
<span className="text-warning">Limit reached raise Max to see more.</span>
))}
<Label className="text-[11px] text-muted-foreground">Max</Label>
<Input
type="number"
@@ -5052,7 +5072,12 @@ export default function App() {
tune the rig. */}
{netEnabled && (
<TabsContent value="net" className="mt-0 flex flex-col min-h-0 flex-1">
<NetControlPanel onLogged={refresh} countries={countries} bands={bands} modes={modes} />
<NetControlPanel onLogged={refresh} countries={countries} bands={bands} modes={modes}
qsoMenuHandlers={{
onUpdateFromCty: bulkUpdateFromCty, onUpdateFromQRZ: bulkUpdateFromQRZ, onUpdateFromClublog: bulkUpdateFromClublog,
onSendTo: bulkSendTo, onSendRecording: bulkSendRecording, onSendEQSL: (ids: number[]) => setEqslQsoId(ids[0] ?? null),
onDelete: (ids: number[]) => setDeletingIds(ids),
}} />
</TabsContent>
)}