feat(cluster): the server pills become the per-server switch

The colour already said connected; the word beside it said it again, in
the widest part of a row that has to hold five servers. It goes, and the
pills earn the job nothing else in the app had: reaching ONE cluster
without opening Settings. Click connects or drops that session — a
connecting one included, so a server retrying into a dead host can be
stopped. State, retry count, address and last error move into the
tooltip, where they cost no width.
This commit is contained in:
2026-08-31 19:03:32 +02:00
parent 2f1d592497
commit d2194da28e
3 changed files with 28 additions and 11 deletions
+4 -2
View File
@@ -14,7 +14,8 @@
"QSO editor, QSL Info: a HamQTH channel and its row in the status table — sent only, the received column showing a dash since the site publishes no confirmations.",
"QSO editor, QSL Info: the confirmation channels are listed paper QSL and LoTW first — the two that carry an ARRL award — then alphabetically, in both the picker and the status table.",
"Band map: a chevron in the footer folds the colour legend away and brings it back — four lines of a short screen, remembered between sessions.",
"Band map: ctrl+wheel no longer zooms it — that gesture is the window zoom everywhere else in OpsLog. The + and buttons keep the zoom."
"Band map: ctrl+wheel no longer zooms it — that gesture is the window zoom everywhere else in OpsLog. The + and buttons keep the zoom.",
"DX Cluster: the server pills drop the CONNECTED/DISCONNECTED word — the colour already said it — and clicking one now connects or disconnects that server on its own, without opening Settings. State, retries, address and last error moved into the tooltip."
],
"fr": [
"Changer de base de réglages naffiche plus « OpsLog is already running » : la relance automatique attend désormais que linstance qui se ferme libère son verrou au lieu de la prendre de vitesse.",
@@ -28,7 +29,8 @@
"Éditeur de QSO, onglet QSL : un canal HamQTH et sa ligne dans le tableau des statuts — envoi seulement, la colonne reçu affichant un tiret puisque le site ne publie aucune confirmation.",
"Éditeur de QSO, onglet QSL : les canaux de confirmation sont classés QSL papier puis LoTW — les deux qui comptent pour un diplôme ARRL — puis par ordre alphabétique, dans le sélecteur comme dans le tableau.",
"Band map : un chevron dans le pied de page replie la légende des couleurs et la fait revenir — quatre lignes gagnées sur un petit écran, mémorisé dune session à lautre.",
"Band map : ctrl+molette ne zoome plus la carte — ce geste est le zoom de la fenêtre partout ailleurs dans OpsLog. Les boutons + et gardent le zoom."
"Band map : ctrl+molette ne zoome plus la carte — ce geste est le zoom de la fenêtre partout ailleurs dans OpsLog. Les boutons + et gardent le zoom.",
"DX Cluster : les pastilles de serveur perdent le mot CONNECTED/DISCONNECTED — la couleur le disait déjà — et cliquer sur lune connecte ou déconnecte ce serveur seul, sans passer par les réglages. État, tentatives, adresse et dernière erreur passent dans linfobulle."
]
},
{
+22 -7
View File
@@ -27,7 +27,7 @@ import {
GetTunerGeniusStatus, GetTunerGeniusSettings, TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, TunerGeniusActivate,
GetScpStatus, ScpLookup,
OpenExternalURL,
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, SendClusterCommand,
ConnectAllClusters, DisconnectAllClusters, ConnectClusterServer, DisconnectClusterServer, GetClusterStatus, SendClusterCommand,
ListClusterServers, ClusterSpotStatuses, SendClusterSpot,
GetCATSettings, PTTHotkeyDown, PTTHotkeyUp,
GetSolarData,
@@ -8133,22 +8133,37 @@ export default function App() {
const isMaster = clusterServers
.filter((x) => x.enabled)
.sort((a, b) => a.sort_order - b.sort_order)[0]?.id === s.server_id;
// The colour already says the state, so the word beside it was
// saying it twice — and the pills are the only place a single
// cluster can be reached without opening Settings. Clicking one
// now drops or reopens that session; everything the word used
// to carry (state, retries, last error, address) moves into the
// tooltip, where it costs no width.
const up = s.state === 'connected';
const busy = s.state === 'connecting' || s.state === 'reconnecting';
return (
<span
<button
key={s.server_id}
type="button"
onClick={async () => {
try {
if (up || busy) await DisconnectClusterServer(s.server_id);
else await ConnectClusterServer(s.server_id);
} catch (e: any) { setError(String(e?.message ?? e)); }
await reloadClusterMeta();
}}
className={cn(
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold border',
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold border transition-opacity hover:opacity-80',
s.state === 'connected' ? 'bg-success-muted text-success-muted-foreground border-success-border' :
s.state === 'connecting' || s.state === 'reconnecting' ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' :
busy ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' :
s.state === 'error' ? 'bg-danger-muted text-danger-muted-foreground border-danger-border' :
'bg-muted text-muted-foreground border-border',
)}
title={`${s.host}:${s.port}${s.error ? ' — ' + s.error : ''}`}
title={`${s.name}${s.state.toUpperCase()}${s.retries ? ` #${s.retries}` : ''} · ${s.host}:${s.port}${s.error ? ' — ' + s.error : ''}\n${up || busy ? t('clu.pillDisconnect') : t('clu.pillConnect')}`}
>
{isMaster && <span className="text-warning" title="Master (commands go here)"></span>}
{s.name}
<span className="opacity-60 text-[9px] ml-0.5">{s.state.toUpperCase()}{s.retries ? ` #${s.retries}` : ''}</span>
</span>
</button>
);
})}
<div className="flex-1" />
+2 -2
View File
@@ -347,7 +347,7 @@ const en: Dict = {
'chg.mode': 'Chase', 'chg.sources': 'Confirmed by', 'chg.card': 'QSL card', 'dec.unconfTip': 'Worked but not confirmed — a QSL to chase', 'gsc.scope': 'Match a square by', 'gsc.hunt': 'Chase', 'gsc.huntNew': 'New — never worked', 'gsc.huntUnconf': 'New and unconfirmed', 'gsc.scope_band_digi': 'This band + any digital mode', 'gsc.scope_band_mode': 'This band + this exact mode', 'gsc.scope_band_ftx': 'This band + any FT mode (FT8/FT4/FT2)', 'gsc.scope_mix_digi': 'Any band + any digital mode', 'gsc.scope_mix_mode': 'Any band + this exact mode', 'gsc.scope_mix_ftx': 'Any band + any FT mode (FT8/FT4/FT2)', 'gsc.hint': 'Decides when a square stops being NEW. Narrower means more squares to chase: per band and per exact mode is the most demanding, any band and any digital mode the least. Chasing unconfirmed as well keeps a square wanted until a QSL, LoTW or eQSL confirmation arrives — it is still missing from the award until then.',
'gsm.basemap': 'Basemap', 'gsm.title': 'Grid squares', 'gsm.all': 'All', 'gsm.phone': 'Phone', 'gsm.cw': 'CW', 'gsm.digital': 'Digital', 'gsm.ftx': 'FTx', 'gsm.confirmed': 'confirmed', 'gsm.worked': 'worked', 'gsm.colConfirmed': 'Colour for confirmed squares', 'gsm.colWorked': 'Colour for worked (unconfirmed) squares', 'gsm.colReset': 'Back to the theme colours', 'gsm.refresh': 'Recount from the log', 'gsm.count': '{n} squares · {c} confirmed',
'bo.nearKm': 'Count receivers within', 'bo.nearKmHint': 'A report proves YOUR path only if it was collected near you. Smaller is more local but leaves fewer receivers listening — too small and the watch has nothing to look at. 300 km borrows a whole region; 100 km suits 2 m, where a duct is narrow.',
'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.chasePota': 'Chase POTA', 'clu.chasePotaHint': 'Off: no NEW POTA badge or filter, and the POTA column stays empty — a new-band + new-POTA spot reads NEW BAND alone.', 'clu.chaseSota': 'Chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseCounty': 'Chase US counties', 'clu.chaseCountyHint': 'Off: no NEW COUNTY badge or filter in the cluster.', 'clu.chasePfx': 'Chase new prefixes', 'clu.chasePfxHint': 'Off: no NEW PFX badge or filter in the cluster.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot',
'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.pillConnect': 'Click to connect', 'clu.pillDisconnect': 'Click to disconnect', 'clu.chasePota': 'Chase POTA', 'clu.chasePotaHint': 'Off: no NEW POTA badge or filter, and the POTA column stays empty — a new-band + new-POTA spot reads NEW BAND alone.', 'clu.chaseSota': 'Chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseCounty': 'Chase US counties', 'clu.chaseCountyHint': 'Off: no NEW COUNTY badge or filter in the cluster.', 'clu.chasePfx': 'Chase new prefixes', 'clu.chasePfxHint': 'Off: no NEW PFX badge or filter in the cluster.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot',
'clu.macros': 'Command buttons', 'clu.macrosHint': 'A named button beside the cluster command box. Leave the command empty and the button is not shown.',
'clu.macroLabel': 'Button', 'clu.macroCmd': 'Command',
'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere.',
@@ -863,7 +863,7 @@ const fr: Dict = {
'bo.nearKm': 'Compter les récepteurs à moins de', 'bo.nearKmHint': 'Un report ne prouve TON chemin que sil a été collecté près de chez toi. Plus petit est plus local, mais laisse moins de récepteurs à l’écoute — trop petit, la veille na plus rien à observer. 300 km emprunte les oreilles de toute une région ; 100 km convient au 2 m, où un conduit est étroit.',
'bo.open': 'ouvert', 'bo.liveTip': '{band} est ouvert — {n} stations, ~{km} km, {sector}{season}. Cliquer pour le bandmap.', 'bo.enable': 'Surveiller les ouvertures de bande', 'bo.enableHint': "(10, 12, 6, 4 et 2 m. Activer ajoute les deux nœuds RBN et souscrit au flux PSK Reporter — la détection a besoin de bien plus d oreilles qu un cluster ne peut en fournir.)", 'bo.feedUp': 'Flux PSK Reporter actif — {n} décodages vus', 'bo.feedDown': 'Flux PSK Reporter inactif — il faut ton locator, et un instant pour se connecter', 'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot',
'clu.macros': 'Boutons de commande', 'clu.macrosHint': 'Un bouton nommé à côté du champ de commande du cluster. Laisse la commande vide et le bouton nest pas affiché.',
'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.chasePota': 'Chasser le POTA', 'clu.chasePotaHint': "Décoché : plus de badge ni de filtre NOUVEAU POTA, et la colonne POTA reste vide — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE.", 'clu.chaseSota': 'Chasser le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseCounty': 'Chasser les comtés US', 'clu.chaseCountyHint': 'Décoché : plus de badge ni de filtre NOUVEAU COMTÉ dans le cluster.', 'clu.chasePfx': 'Chasser les nouveaux préfixes', 'clu.chasePfxHint': 'Décoché : plus de badge ni de filtre NOUVEAU PFX dans le cluster.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture',
'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.pillConnect': 'Cliquer pour connecter', 'clu.pillDisconnect': 'Cliquer pour déconnecter', 'clu.chasePota': 'Chasser le POTA', 'clu.chasePotaHint': "Décoché : plus de badge ni de filtre NOUVEAU POTA, et la colonne POTA reste vide — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE.", 'clu.chaseSota': 'Chasser le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseCounty': 'Chasser les comtés US', 'clu.chaseCountyHint': 'Décoché : plus de badge ni de filtre NOUVEAU COMTÉ dans le cluster.', 'clu.chasePfx': 'Chasser les nouveaux préfixes', 'clu.chasePfxHint': 'Décoché : plus de badge ni de filtre NOUVEAU PFX dans le cluster.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture',
'clu.workedSameSlotHint': "— un spot n'est « contacté » que si cet indicatif a été fait sur la MÊME bande et le même mode, pas n'importe où.",