Compare commits
6
Commits
d0f25aea9b
...
v0.23.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2de5bce998 | ||
|
|
f0959c796d | ||
|
|
16e64fd7b7 | ||
|
|
7e696a72bb | ||
|
|
9af2732314 | ||
|
|
f82d92565f |
+10
-2
@@ -15,7 +15,11 @@
|
|||||||
"Selecting a QSO in the Recent QSOs list now shows that station in the Stats (F1) matrix, instead of leaving it blank.",
|
"Selecting a QSO in the Recent QSOs list now shows that station in the Stats (F1) matrix, instead of leaving it blank.",
|
||||||
"Stats (F1): click any coloured band/mode square to list the contacts behind it.",
|
"Stats (F1): click any coloured band/mode square to list the contacts behind it.",
|
||||||
"Awards: the DXCC list now shows each entity’s main prefix (XE, DL, F…) in its own sortable, searchable column.",
|
"Awards: the DXCC list now shows each entity’s main prefix (XE, DL, F…) in its own sortable, searchable column.",
|
||||||
"DX Cluster: new option \"Already worked only on the same slot\" (Settings → DX Cluster). With it on, a spot reads \"worked\" only when you worked that callsign on the SAME band and mode — not just anywhere. It respects the digital-mode grouping (Settings → General): grouped, a 20m FT8 contact also marks a 20m FT4 spot as worked; ungrouped, FT8 and FT4 are separate slots."
|
"DX Cluster: new option \"Already worked only on the same slot\" (Settings → DX Cluster). With it on, a spot reads \"worked\" only when you worked that callsign on the SAME band and mode — not just anywhere. It respects the digital-mode grouping (Settings → General): grouped, a 20m FT8 contact also marks a 20m FT4 spot as worked; ungrouped, FT8 and FT4 are separate slots.",
|
||||||
|
"DX Cluster: added a WORKED status-filter chip, so you can show — or, with the other chips off, isolate — already-worked spots, not only hide them with the \"Hide worked\" checkbox.",
|
||||||
|
"DX Cluster: spots that represent nothing — entity/band/mode already worked, the callsign not in your log, no POTA/county/prefix novelty — are now dimmed, so your eye skips them and the spots worth working stand out. What counts as \"nothing\" follows the \"same slot\" option and the digital-mode grouping.",
|
||||||
|
"Ultrabeam: an older controller (seen over an RS232-to-Ethernet bridge) answers with an 11-byte status frame instead of 12. OpsLog rejected it as \"too short\" and reconnect-looped; it now accepts a shorter checksum-valid frame and defaults the missing tail field.",
|
||||||
|
"Fixed a crash (React #300) that could take down the whole window when the rig briefly reported an unusual band — for example a spurious 33 cm reading from an Icom. The Band Map had a keyboard-navigation hook after its early return for unknown bands, so the number of hooks changed between renders. The hook now always runs."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Visionneuse de log : la fenêtre conserve deux fois plus d'historique (512 Ko au lieu de 256 Ko, ~3200 lignes). Lors d'une trace chargée, les plus vieilles lignes défilaient hors du buffer pendant qu'on les lisait encore ; la fenêtre agrandie les garde.",
|
"Visionneuse de log : la fenêtre conserve deux fois plus d'historique (512 Ko au lieu de 256 Ko, ~3200 lignes). Lors d'une trace chargée, les plus vieilles lignes défilaient hors du buffer pendant qu'on les lisait encore ; la fenêtre agrandie les garde.",
|
||||||
@@ -30,7 +34,11 @@
|
|||||||
"Sélectionner un QSO dans la liste des QSO récents affiche désormais cette station dans la matrice Stats (F1), au lieu de la laisser vide.",
|
"Sélectionner un QSO dans la liste des QSO récents affiche désormais cette station dans la matrice Stats (F1), au lieu de la laisser vide.",
|
||||||
"Stats (F1) : cliquer sur une case bande/mode colorée liste les contacts correspondants.",
|
"Stats (F1) : cliquer sur une case bande/mode colorée liste les contacts correspondants.",
|
||||||
"Diplômes : la liste DXCC affiche le préfixe principal de chaque entité (XE, DL, F…) dans une colonne triable et cherchable.",
|
"Diplômes : la liste DXCC affiche le préfixe principal de chaque entité (XE, DL, F…) dans une colonne triable et cherchable.",
|
||||||
"DX Cluster : nouvelle option « Déjà contacté seulement sur le même slot » (Réglages → DX Cluster). Activée, un spot n'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode — pas juste n'importe où. Elle respecte le groupage des modes numériques (Réglages → Général) : groupé, un contact 20m FT8 marque aussi un spot 20m FT4 comme contacté ; dégroupé, FT8 et FT4 sont des slots distincts."
|
"DX Cluster : nouvelle option « Déjà contacté seulement sur le même slot » (Réglages → DX Cluster). Activée, un spot n'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode — pas juste n'importe où. Elle respecte le groupage des modes numériques (Réglages → Général) : groupé, un contact 20m FT8 marque aussi un spot 20m FT4 comme contacté ; dégroupé, FT8 et FT4 sont des slots distincts.",
|
||||||
|
"DX Cluster : ajout d'un filtre de statut WORKED, pour afficher — ou, en désactivant les autres, isoler — les spots déjà contactés, pas seulement les masquer avec la case « Masquer les contactés ».",
|
||||||
|
"DX Cluster : les spots qui ne représentent rien — entité/bande/mode déjà faite, indicatif absent du journal, aucune nouveauté POTA/comté/préfixe — sont désormais atténués, pour que l'œil les ignore et que les spots à travailler ressortent. Ce qui compte comme « rien » suit l'option « même slot » et le groupage des modes numériques.",
|
||||||
|
"Ultrabeam : un contrôleur plus ancien (vu derrière un pont RS232-Ethernet) répond avec une trame de statut de 11 octets au lieu de 12. OpsLog la rejetait comme « trop courte » et bouclait en reconnexion ; il accepte désormais une trame valide plus courte et met par défaut le champ de fin manquant.",
|
||||||
|
"Correction d'un plantage (React #300) qui pouvait faire tomber toute la fenêtre quand la radio rapportait brièvement une bande inhabituelle — par exemple une lecture 33 cm parasite d'un Icom. La Band Map avait un hook de navigation clavier après son retour anticipé pour les bandes inconnues, changeant le nombre de hooks entre deux rendus. Le hook est désormais toujours exécuté."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-6
@@ -4490,11 +4490,14 @@ export default function App() {
|
|||||||
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
|
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
|
||||||
const e = spotStatus[k];
|
const e = spotStatus[k];
|
||||||
const st = (e?.status || '') as SpotStatusKey;
|
const st = (e?.status || '') as SpotStatusKey;
|
||||||
// A previously-worked call counts as WORKED for filtering even when its
|
// WORKED means "I've worked THIS callsign" — the blue WKD-CALL flag —
|
||||||
// entity status is still new-band/new-slot (the grid flags it WKD CALL),
|
// NOT the entity status 'worked' (entity/band/mode already worked, which
|
||||||
// matching the "Hide worked" toggle. Additive: it still matches its own
|
// the grid shows as a plain "—"). So the 'worked' key is matched ONLY via
|
||||||
// entity status too, so it stays visible under NEW BAND / NEW SLOT.
|
// worked_call below; exclude the entity status 'worked' from the generic
|
||||||
const matches = clusterStatusFilter.has(st)
|
// match, or every "already-worked-entity" spot would slip through. A
|
||||||
|
// worked call still matches its own entity status too (new-band/new-slot),
|
||||||
|
// so it stays visible under those chips.
|
||||||
|
const matches = (st !== 'worked' && clusterStatusFilter.has(st))
|
||||||
|| (!!e?.worked_call && clusterStatusFilter.has('worked'))
|
|| (!!e?.worked_call && clusterStatusFilter.has('worked'))
|
||||||
|| (!!e?.new_pota && clusterStatusFilter.has('new-pota'))
|
|| (!!e?.new_pota && clusterStatusFilter.has('new-pota'))
|
||||||
|| (!!e?.new_county && clusterStatusFilter.has('new-county'))
|
|| (!!e?.new_county && clusterStatusFilter.has('new-county'))
|
||||||
@@ -4619,7 +4622,10 @@ export default function App() {
|
|||||||
{ k: 'new-pota' as SpotFilterKey, label: 'NEW POTA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
{ k: 'new-pota' as SpotFilterKey, label: 'NEW POTA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
||||||
{ k: 'new-county' as SpotFilterKey, label: 'NEW COUNTY', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
{ k: 'new-county' as SpotFilterKey, label: 'NEW COUNTY', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
||||||
{ k: 'new-pfx' as SpotFilterKey, label: 'NEW PFX', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
|
{ k: 'new-pfx' as SpotFilterKey, label: 'NEW PFX', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
|
||||||
// (no WORKED chip — use the "Hide worked" checkbox to drop dupes.)
|
// Blue, like the already-worked call in the grid. Selecting it keeps
|
||||||
|
// worked spots; the separate "Hide worked" checkbox drops them — they
|
||||||
|
// are opposite controls, so don't use both at once.
|
||||||
|
{ k: 'worked' as SpotFilterKey, label: 'WORKED', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||||
]).map((s) => {
|
]).map((s) => {
|
||||||
const on = clusterStatusFilter.has(s.k);
|
const on = clusterStatusFilter.has(s.k);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -358,6 +358,43 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
|||||||
return () => el.removeEventListener('wheel', onWheel);
|
return () => el.removeEventListener('wheel', onWheel);
|
||||||
}, [range]);
|
}, [range]);
|
||||||
|
|
||||||
|
// Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune
|
||||||
|
// to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot.
|
||||||
|
// Only active on the docked Main-view map (keyNav) and ignored while typing.
|
||||||
|
// MUST stay ABOVE the `if (!range)` early return below: on an unknown band
|
||||||
|
// (e.g. a spurious 33 cm CAT reading from an Icom) that return skipped this
|
||||||
|
// hook, so the hook count changed between renders and React crashed with #300
|
||||||
|
// ("rendered fewer hooks than expected"). lo/hi are always defined (they fall
|
||||||
|
// back to [0,1] when there's no range), so it's safe to run here.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!keyNav) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (!e.ctrlKey || e.altKey || e.metaKey) return;
|
||||||
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||||
|
const ae = document.activeElement as HTMLElement | null;
|
||||||
|
const tag = (ae?.tagName || '').toLowerCase();
|
||||||
|
if (tag === 'input' || tag === 'textarea' || tag === 'select' || ae?.isContentEditable) return;
|
||||||
|
const list = spots
|
||||||
|
.filter((s) => (s.band ?? '') === band && s.freq_hz > 0)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.freq_hz - b.freq_hz);
|
||||||
|
if (!list.length) return;
|
||||||
|
const cur = currentFreqHz || (lo + hi) * 500; // mid-band kHz→Hz when no rig freq
|
||||||
|
const EPS = 50; // Hz, so we don't re-pick the spot we're already sitting on
|
||||||
|
let target: Spot | undefined;
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
target = list.find((s) => s.freq_hz > cur + EPS);
|
||||||
|
} else {
|
||||||
|
for (let i = list.length - 1; i >= 0; i--) { if (list[i].freq_hz < cur - EPS) { target = list[i]; break; } }
|
||||||
|
}
|
||||||
|
if (!target) return;
|
||||||
|
e.preventDefault();
|
||||||
|
onSpotClick(target);
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [keyNav, spots, band, currentFreqHz, lo, hi, onSpotClick]);
|
||||||
|
|
||||||
if (!range) {
|
if (!range) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-col items-center justify-center text-xs text-muted-foreground p-3 bg-muted/20">
|
<div className="h-full w-full flex flex-col items-center justify-center text-xs text-muted-foreground p-3 bg-muted/20">
|
||||||
@@ -388,38 +425,6 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
|||||||
scrollerRef.current.scrollTop = Math.max(0, y - containerH / 2);
|
scrollerRef.current.scrollTop = Math.max(0, y - containerH / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune
|
|
||||||
// to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot.
|
|
||||||
// Only active on the docked Main-view map (keyNav) and ignored while typing.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!keyNav) return;
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (!e.ctrlKey || e.altKey || e.metaKey) return;
|
|
||||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
|
||||||
const ae = document.activeElement as HTMLElement | null;
|
|
||||||
const tag = (ae?.tagName || '').toLowerCase();
|
|
||||||
if (tag === 'input' || tag === 'textarea' || tag === 'select' || ae?.isContentEditable) return;
|
|
||||||
const list = spots
|
|
||||||
.filter((s) => (s.band ?? '') === band && s.freq_hz > 0)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.freq_hz - b.freq_hz);
|
|
||||||
if (!list.length) return;
|
|
||||||
const cur = currentFreqHz || (lo + hi) * 500; // mid-band kHz→Hz when no rig freq
|
|
||||||
const EPS = 50; // Hz, so we don't re-pick the spot we're already sitting on
|
|
||||||
let target: Spot | undefined;
|
|
||||||
if (e.key === 'ArrowUp') {
|
|
||||||
target = list.find((s) => s.freq_hz > cur + EPS);
|
|
||||||
} else {
|
|
||||||
for (let i = list.length - 1; i >= 0; i--) { if (list[i].freq_hz < cur - EPS) { target = list[i]; break; } }
|
|
||||||
}
|
|
||||||
if (!target) return;
|
|
||||||
e.preventDefault();
|
|
||||||
onSpotClick(target);
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', onKey);
|
|
||||||
return () => window.removeEventListener('keydown', onKey);
|
|
||||||
}, [keyNav, spots, band, currentFreqHz, lo, hi, onSpotClick]);
|
|
||||||
|
|
||||||
const currentKHz = currentFreqHz ? currentFreqHz / 1000 : 0;
|
const currentKHz = currentFreqHz ? currentFreqHz / 1000 : 0;
|
||||||
const showRigPointer = currentKHz >= lo && currentKHz <= hi;
|
const showRigPointer = currentKHz >= lo && currentKHz <= hi;
|
||||||
const rigY = freqToY(currentKHz);
|
const rigY = freqToY(currentKHz);
|
||||||
|
|||||||
@@ -140,6 +140,19 @@ function statusColor(s: SpotStatusEntry | undefined): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDull reports a spot that "represents nothing" under the current worked/slot
|
||||||
|
// rules: the entity/band/mode is already worked (status resolved, not new in any
|
||||||
|
// dimension), the callsign itself isn't in the log, and there's no POTA / county
|
||||||
|
// / prefix novelty. Such rows are dimmed so the eye skips them. Because the
|
||||||
|
// status obeys the DX-cluster "same slot" option and the digital-mode grouping,
|
||||||
|
// what counts as dull follows those settings automatically. Unresolved statuses
|
||||||
|
// (still loading, or entity unknown) are NOT dimmed — that would flicker.
|
||||||
|
function isDull(s: SpotStatusEntry | undefined): boolean {
|
||||||
|
if (!s || !s.status) return false;
|
||||||
|
if (s.status === 'new' || s.status === 'new-band' || s.status === 'new-mode' || s.status === 'new-slot') return false;
|
||||||
|
return !(s.worked_call || s.new_pota || s.new_county || s.new_pfx);
|
||||||
|
}
|
||||||
|
|
||||||
const makeColCatalog = (t: TFn): ColEntry[] => [
|
const makeColCatalog = (t: TFn): ColEntry[] => [
|
||||||
{
|
{
|
||||||
group: 'Spot', label: t('clg2.c.time'), colId: 'time',
|
group: 'Spot', label: t('clg2.c.time'), colId: 'time',
|
||||||
@@ -410,7 +423,9 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) {
|
|||||||
// won't re-render those cells on its own — force a refresh so e.g. a worked call
|
// won't re-render those cells on its own — force a refresh so e.g. a worked call
|
||||||
// turns blue once its status loads.
|
// turns blue once its status loads.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gridRef.current?.api?.refreshCells({ force: true });
|
// redrawRows (not refreshCells) so getRowStyle re-runs too — the whole-row
|
||||||
|
// dimming of "represents nothing" spots depends on the status that lands here.
|
||||||
|
gridRef.current?.api?.redrawRows();
|
||||||
}, [spotStatus]);
|
}, [spotStatus]);
|
||||||
|
|
||||||
// Restore AFTER the profile scope is known — this grid has no key= remount to
|
// Restore AFTER the profile scope is known — this grid has no key= remount to
|
||||||
@@ -496,6 +511,7 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) {
|
|||||||
onColumnVisible={saveColumnState}
|
onColumnVisible={saveColumnState}
|
||||||
onSortChanged={saveColumnState}
|
onSortChanged={saveColumnState}
|
||||||
onRowClicked={handleRowClicked}
|
onRowClicked={handleRowClicked}
|
||||||
|
getRowStyle={(p: any) => (isDull(statusFor(p)) ? { opacity: 0.4 } : undefined)}
|
||||||
animateRows={false}
|
animateRows={false}
|
||||||
suppressCellFocus
|
suppressCellFocus
|
||||||
getRowId={(p) => `${(p.data as any).received_at}-${(p.data as any).dx_call}-${(p.data as any).source_id}`}
|
getRowId={(p) => `${(p.data as any).received_at}-${(p.data as any).dx_call}-${(p.data as any).source_id}`}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Single source of truth for the app version shown in the UI (header + About).
|
// Single source of truth for the app version shown in the UI (header + About).
|
||||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||||
export const APP_VERSION = '0.23.5';
|
export const APP_VERSION = '0.23.6';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
@@ -509,7 +509,13 @@ func (c *Client) queryStatus() (*Status, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reply) < 12 {
|
// An older controller — seen behind an RS232-to-Ethernet bridge — answers with
|
||||||
|
// an 11-byte status frame: the standard one WITHOUT the trailing FreqMax byte.
|
||||||
|
// The packet checksum was already verified, so a short-but-valid frame is real,
|
||||||
|
// not a fragment. Accept it and default the missing tail fields instead of
|
||||||
|
// reconnect-looping on "reply too short". reply[9]/[10] (MotorsMoving, FreqMin)
|
||||||
|
// are the last we truly need, so 10 bytes is the floor.
|
||||||
|
if len(reply) < 10 {
|
||||||
return nil, fmt.Errorf("status reply too short: %d bytes", len(reply))
|
return nil, fmt.Errorf("status reply too short: %d bytes", len(reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,8 +528,12 @@ func (c *Client) queryStatus() (*Status, error) {
|
|||||||
Direction: int(reply[6] & 0x0F),
|
Direction: int(reply[6] & 0x0F),
|
||||||
OffState: (reply[7] & 0x02) != 0,
|
OffState: (reply[7] & 0x02) != 0,
|
||||||
MotorsMoving: int(reply[9]),
|
MotorsMoving: int(reply[9]),
|
||||||
FreqMin: int(reply[10]),
|
}
|
||||||
FreqMax: int(reply[11]),
|
if len(reply) > 10 {
|
||||||
|
status.FreqMin = int(reply[10])
|
||||||
|
}
|
||||||
|
if len(reply) > 11 {
|
||||||
|
status.FreqMax = int(reply[11])
|
||||||
}
|
}
|
||||||
|
|
||||||
return status, nil
|
return status, nil
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||||
appVersion = "0.23.5"
|
appVersion = "0.23.6"
|
||||||
|
|
||||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||||
// to https://us.i.posthog.com for a US project.
|
// to https://us.i.posthog.com for a US project.
|
||||||
|
|||||||
Reference in New Issue
Block a user