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
+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" />