fix: simplify host status to online/offline and keep it live without a refresh

This commit is contained in:
LukeGus committed 2026-10-04 14:18:50 -05:00
1 parent 2646b649d7
commit 0a436ab3d8
48 files changed
+549 -789

No files matched your search

+1 -1
View File
@@ -117,7 +117,7 @@ export function AdminSettingsPanel({
const [passwordLoginForced, setPasswordLoginForced] = useState(false);
const [allowPasswordReset, setAllowPasswordReset] = useState(true);
const [sessionTimeout, setSessionTimeout] = useState("24");
const [statusInterval, setStatusInterval] = useState("60");
const [statusInterval, setStatusInterval] = useState("30");
const [logLevel, setLogLevel] = useState("info");
const [notificationPrivateEndpoints, setNotificationPrivateEndpoints] =
useState<string[]>([]);
+13 -6
View File
@@ -1,7 +1,7 @@
import { getErrorMessage } from "../lib/error-message.js";
import { enabledHostProtocols, useHostProtocols } from "./host-protocols";
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { HostData } from "@/types/index";
import { useTranslation } from "react-i18next";
import {
@@ -53,6 +53,8 @@ import {
import type { SSHHostWithStatus } from "@/main-axios";
import type { Host, HostFolder, TabType } from "@/types/ui-types";
import { sortHostTree, type SortKey } from "@/sidebar/host-sort";
import { withLiveStatus } from "@/sidebar/live-host-status";
import { useServerStatus } from "@/lib/ServerStatusContext";
import { useHostSidebarPreferences } from "@/sidebar/tree/hooks/useHostSidebarPreferences";
import { useArrangeLock } from "@/sidebar/use-arrange-lock";
import type {
@@ -137,9 +139,7 @@ function hostPassesFilters(
if (filters.status.length > 0) {
const ok =
(filters.status.includes("online") && host.online) ||
(filters.status.includes("offline") &&
host.status !== "reachable" &&
!host.online) ||
(filters.status.includes("offline") && host.status === "offline") ||
(filters.status.includes("pinned") && !!host.pin);
if (!ok) return false;
}
@@ -219,6 +219,13 @@ export function HostsPanel({
active?: boolean;
}) {
const { t } = useTranslation();
const { statuses, getStatus } = useServerStatus();
const liveHostTree = useMemo(
() => (hostTree ? withLiveStatus(hostTree, getStatus) : undefined),
// statuses changes identity whenever a host's status changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[hostTree, statuses, getStatus],
);
const hostSwitchPlugins = usePluginHostSections().filter(
(plugin) => !!plugin.contributes?.settings?.host?.enableKey,
);
@@ -1028,10 +1035,10 @@ export function HostsPanel({
<div className="flex flex-col flex-1 min-h-0">
<SidebarTree
children={
hostTree
liveHostTree
? groupHosts(
applyFilters(
sortHostTree(hostTree, sortKey, pinnedFirst),
sortHostTree(liveHostTree, sortKey, pinnedFirst),
filterState,
hostSwitches,
),
+36
View File
@@ -0,0 +1,36 @@
import type { Host, HostFolder } from "@/types/ui-types";
import type { StatusValue } from "@/lib/server-status-store";
import { isFolder } from "./tree/visible-rows";
/**
* Copies each host's live status onto the tree so sorting, filtering and
* grouping by status read the same value as the row's dot. A host with
* status checks off gets no status.
*/
export function withLiveStatus(
folder: HostFolder,
getStatus: (hostId: number) => StatusValue,
): HostFolder {
return {
...folder,
children: folder.children.map((child) => {
if (isFolder(child)) return withLiveStatus(child, getStatus);
const host = child as Host;
const status =
host.statusCheckEnabled === false
? undefined
: getStatus(Number(host.id));
if (host.status === status) return host;
return { ...host, status, online: status === "online" };
}),
};
}
/** The flat-list version of withLiveStatus. */
export function withLiveHostStatus(
hosts: Host[],
getStatus: (hostId: number) => StatusValue,
): Host[] {
return withLiveStatus({ name: "", children: hosts }, getStatus)
.children as Host[];
}
+9 -22
View File
@@ -15,23 +15,12 @@ import { HostItem, statusCheckEnabled } from "../HostItem/HostItem";
import { isFolder, folderHasMatch, collectAllHosts } from "../visible-rows";
import { FolderActions } from "./FolderActions";
export function folderHostCount(folder: HostFolder): {
total: number;
online: number;
} {
let total = 0,
online = 0;
export function folderHostCount(folder: HostFolder): number {
let total = 0;
for (const child of folder.children) {
if (isFolder(child)) {
const c = folderHostCount(child);
total += c.total;
online += c.online;
} else {
total++;
if (child.online) online++;
}
total += isFolder(child) ? folderHostCount(child) : 1;
}
return { total, online };
return total;
}
export function FolderItem({
@@ -140,13 +129,11 @@ export function FolderItem({
onReorderHoverChange?: (edge: "before" | "after" | null) => void;
}) {
const { t } = useTranslation();
const { getStatus, initialLoadComplete } = useServerStatus();
const { total } = folderHostCount(folder);
const online = initialLoadComplete
? collectAllHosts(folder.children).filter(
(h) => statusCheckEnabled(h) && getStatus(Number(h.id)) === "online",
).length
: folderHostCount(folder).online;
const { getStatus } = useServerStatus();
const total = folderHostCount(folder);
const online = collectAllHosts(folder.children).filter(
(h) => statusCheckEnabled(h) && getStatus(Number(h.id)) === "online",
).length;
const [dragOver, setDragOver] = useState(false);
const reorderEdge = isReorderHovered ? reorderHoverEdge : null;
+11 -28
View File
@@ -59,11 +59,7 @@ import {
useStatusColorScheme,
getStatusClasses,
} from "@/hooks/use-status-color-scheme";
import {
useHostStatus,
useHostStatusReason,
useServerStatusMeta,
} from "@/lib/ServerStatusContext";
import { useHostStatus } from "@/lib/ServerStatusContext";
import {
Tooltip,
TooltipContent,
@@ -101,16 +97,16 @@ export function statusCheckEnabled(host: Host): boolean {
export function buildStatusTooltip(
host: Host,
status: "online" | "reachable" | "offline",
status: "online" | "offline" | "unknown",
t: (key: string) => string = (k) => k,
): string {
if (!statusCheckEnabled(host)) return t("hosts.status.monitoringDisabled");
const statusLabel =
status === "online"
? t("hosts.status.available")
: status === "reachable"
? t("hosts.status.reachable")
: t("hosts.status.offline");
if (!statusCheckEnabled(host)) return t("hosts.status.monitoringDisabled");
? t("hosts.status.online")
: status === "offline"
? t("hosts.status.offline")
: t("hosts.status.checking");
const protocols: string[] = [];
if (host.enableSsh) protocols.push("SSH");
for (const protocol of enabledHostProtocols(host)) {
@@ -283,22 +279,11 @@ export function HostItem({
const pluginMenuItems = hostMenuItemsFor(useHostContextMenuItems(), host);
const splitTargets = useSplitTargets();
const statusScheme = useStatusColorScheme();
const { initialLoadComplete } = useServerStatusMeta();
const statusCheckOn = statusCheckEnabled(host);
const statusLoading = !initialLoadComplete && statusCheckOn;
// Per-host subscription — status polls only re-render rows that flipped.
const liveStatus = useHostStatus(Number(host.id), statusCheckOn);
const statusReason = useHostStatusReason(Number(host.id), statusCheckOn);
// Per-host subscription, so a poll only re-renders rows that flipped.
const availability =
liveStatus === "online" ||
liveStatus === "reachable" ||
liveStatus === "offline"
? liveStatus
: host.status === "reachable"
? "reachable"
: host.online
? "online"
: "offline";
useHostStatus(Number(host.id), statusCheckOn) ?? "offline";
const statusLoading = availability === "unknown";
const isOnline = availability === "online";
const previousAvailability = useRef(availability);
const [statusLocking, setStatusLocking] = useState(false);
@@ -1136,9 +1121,7 @@ export function HostItem({
</span>
</TooltipTrigger>
<TooltipContent side="right">
{statusReason === "host_key_changed"
? `${t("hostKey.keyChangedWarning")}: ${t("hostKey.keyChangedDescription")}`
: buildStatusTooltip(host, availability, t)}
{buildStatusTooltip(host, availability, t)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
+1 -1
View File
@@ -502,7 +502,7 @@ export function SidebarTree({
function handleDeleteFolder(folder: HostFolder) {
const folderPath = folder.path ?? folder.name;
const { total } = folderHostCount(folder);
const total = folderHostCount(folder);
setConfirmDialog({
message: t("hosts.deleteFolderConfirm", {
name: folder.name,