mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
fix: simplify host status to online/offline and keep it live without a refresh
This commit is contained in:
1 parent
2646b649d7
commit
0a436ab3d8
48 files changed
+549
-789
No files matched your search
@@ -89,7 +89,7 @@ describe("host maintenance", () => {
|
||||
await service.tick();
|
||||
expect(server.mock.notifications).toHaveLength(0);
|
||||
server.mock.hostStatuses.set(1, {
|
||||
status: "reachable",
|
||||
status: "online",
|
||||
lastChecked: "2026-01-01T13:21Z",
|
||||
});
|
||||
await service.tick();
|
||||
|
||||
@@ -341,13 +341,7 @@ export async function startConnect(
|
||||
return;
|
||||
}
|
||||
|
||||
const { hostKeyChanged, authFailed } = classifyConnectError(
|
||||
message,
|
||||
logs,
|
||||
);
|
||||
if (hostKeyChanged || authFailed) {
|
||||
ctx.hosts.status.reportLogin(hostId, { ok: false, hostKeyChanged });
|
||||
}
|
||||
const { authFailed } = classifyConnectError(message, logs);
|
||||
log.error("Docker SSH connection failed", {
|
||||
hostId,
|
||||
userId,
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("docker routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a failed login to core", async () => {
|
||||
it("never reports a failed login to core", async () => {
|
||||
const client = new FakeClient();
|
||||
server = await startServer({ client });
|
||||
server.mock.ctx.ssh.connect = (async () => {
|
||||
@@ -311,10 +311,6 @@ describe("docker routes", () => {
|
||||
}) as never;
|
||||
const response = await connect(server);
|
||||
expect(response.status).toBe(500);
|
||||
expect(server.mock.statusReports).toContainEqual({
|
||||
hostId: 7,
|
||||
ok: false,
|
||||
hostKeyChanged: false,
|
||||
});
|
||||
expect(server.mock.statusReports).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -23,11 +23,9 @@ function StatusDot({ status }: { status: string }) {
|
||||
const color =
|
||||
status === "online"
|
||||
? getAccentColor()
|
||||
: status === "reachable"
|
||||
? "#fbbf24"
|
||||
: status === "offline"
|
||||
? "#ef4444"
|
||||
: "#6b7280";
|
||||
: status === "offline"
|
||||
? "#ef4444"
|
||||
: "#6b7280";
|
||||
return (
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0 inline-block"
|
||||
|
||||
@@ -52,23 +52,20 @@ function HostStatusWidget({
|
||||
);
|
||||
}
|
||||
|
||||
const online = statusInfo?.status === "online";
|
||||
const reachable = statusInfo?.status === "reachable";
|
||||
const status = statusInfo?.status;
|
||||
const online = status === "online";
|
||||
const known = online || status === "offline";
|
||||
|
||||
const onlineColor = !statusInfo
|
||||
const onlineColor = !known
|
||||
? "#6b7280"
|
||||
: online
|
||||
? getAccentColor()
|
||||
: reachable
|
||||
? "#fbbf24"
|
||||
: "#ef4444";
|
||||
const onlineLabel = !statusInfo
|
||||
: "#ef4444";
|
||||
const onlineLabel = !known
|
||||
? t("common.unknown")
|
||||
: online
|
||||
? t("common.online")
|
||||
: reachable
|
||||
? t("common.signInFailed")
|
||||
: t("common.offline");
|
||||
: t("common.offline");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
||||
|
||||
@@ -347,11 +347,8 @@ export class MetricsPoller {
|
||||
const settings = await this.settingsFor(host.id);
|
||||
if (!settings.metricsEnabled) return;
|
||||
|
||||
let authenticated = false;
|
||||
try {
|
||||
const metrics = await collectMetrics(this.deps, host, settings, () => {
|
||||
authenticated = true;
|
||||
});
|
||||
const metrics = await collectMetrics(this.deps, host, settings);
|
||||
this.metrics.set(host.id, { data: metrics, timestamp: Date.now() });
|
||||
this.ctx.hosts.status.reportLogin(host.id, { ok: true });
|
||||
this.setCollectionState(host, "collecting");
|
||||
@@ -367,14 +364,6 @@ export class MetricsPoller {
|
||||
const hostKeyChanged = isHostKeyVerificationError(error);
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
const isAuthError = /authentication|permission denied/i.test(message);
|
||||
|
||||
// A timeout or a dropped connection says nothing about the login.
|
||||
if (!authenticated && (isAuthError || hostKeyChanged)) {
|
||||
this.ctx.hosts.status.reportLogin(host.id, {
|
||||
ok: false,
|
||||
hostKeyChanged,
|
||||
});
|
||||
}
|
||||
this.setCollectionState(
|
||||
host,
|
||||
hostKeyChanged
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { PluginHostStatus } from "@termix/plugin-sdk/backend";
|
||||
import type { CpuSamples } from "./widgets/cpu-collector.js";
|
||||
import { createNetworkSamples } from "./widgets/network-collector.js";
|
||||
|
||||
export type HostStatus = "online" | "reachable" | "offline";
|
||||
|
||||
export class RequestQueue {
|
||||
private queues = new Map<number, Array<() => Promise<unknown>>>();
|
||||
private processing = new Set<number>();
|
||||
@@ -418,7 +417,7 @@ export class HostPollCache<THost extends { id: number } = { id: number }> {
|
||||
* host answers. A host core does not check (status checks off) is tried.
|
||||
*/
|
||||
export function canStartInitialMetrics(
|
||||
status: HostStatus | null | undefined,
|
||||
status: PluginHostStatus | null | undefined,
|
||||
hasViewers: boolean,
|
||||
): boolean {
|
||||
return hasViewers && status !== "offline";
|
||||
|
||||
@@ -40,7 +40,7 @@ function setup(hostIds = [7]) {
|
||||
hostStatuses: Object.fromEntries(
|
||||
hostIds.map((id) => [
|
||||
id,
|
||||
{ status: "reachable" as const, lastChecked: "now" },
|
||||
{ status: "online" as const, lastChecked: "now" },
|
||||
]),
|
||||
),
|
||||
});
|
||||
@@ -131,13 +131,13 @@ describe("MetricsPoller", () => {
|
||||
expect(collectMetrics).not.toHaveBeenCalled();
|
||||
|
||||
// Core's check comes back: the first sample runs.
|
||||
fake.hostStatuses.set(7, { status: "reachable", lastChecked: "now" });
|
||||
poller.hostStatusChanged(7, "reachable");
|
||||
fake.hostStatuses.set(7, { status: "online", lastChecked: "now" });
|
||||
poller.hostStatusChanged(7, "online");
|
||||
await flush();
|
||||
expect(collectMetrics).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reports a failed login and why collection stopped", async () => {
|
||||
it("says why collection stopped without touching the status", async () => {
|
||||
collectMetrics.mockRejectedValue(
|
||||
new Error("All configured authentication methods failed"),
|
||||
);
|
||||
@@ -146,9 +146,7 @@ describe("MetricsPoller", () => {
|
||||
poller.registerViewer(7, "viewer-a", "user-1");
|
||||
await flush();
|
||||
|
||||
expect(fake.statusReports).toEqual([
|
||||
{ hostId: 7, ok: false, hostKeyChanged: false },
|
||||
]);
|
||||
expect(fake.statusReports).toEqual([]);
|
||||
expect(fake.emitted.at(-1)?.payload).toMatchObject({
|
||||
state: "auth_failed",
|
||||
});
|
||||
@@ -174,9 +172,7 @@ describe("MetricsPoller", () => {
|
||||
poller.registerViewer(7, "viewer-a", "user-1");
|
||||
await flush();
|
||||
|
||||
expect(fake.statusReports).toEqual([
|
||||
{ hostId: 7, ok: false, hostKeyChanged: true },
|
||||
]);
|
||||
expect(fake.statusReports).toEqual([]);
|
||||
expect(state.authFailures.shouldSkip(7)).toBe(true);
|
||||
poller.hostKeyAccepted(7);
|
||||
expect(state.authFailures.shouldSkip(7)).toBe(false);
|
||||
|
||||
@@ -27,7 +27,6 @@ describe("auth failure tracking", () => {
|
||||
describe("initial metrics admission", () => {
|
||||
it("needs a viewer and a host core does not see as offline", () => {
|
||||
expect(canStartInitialMetrics("online", true)).toBe(true);
|
||||
expect(canStartInitialMetrics("reachable", true)).toBe(true);
|
||||
expect(canStartInitialMetrics("offline", true)).toBe(false);
|
||||
expect(canStartInitialMetrics("online", false)).toBe(false);
|
||||
// Status checks off: core has no status, so the sample is tried.
|
||||
|
||||
@@ -73,7 +73,7 @@ const AVAILABLE_COLORS = [
|
||||
{ value: "#6b7280", label: "Gray" },
|
||||
];
|
||||
|
||||
type HostStatus = "online" | "reachable" | "offline" | "unknown";
|
||||
type HostStatus = "online" | "offline" | "unknown";
|
||||
|
||||
interface HostWithStatus extends PluginHostRecord {
|
||||
status?: HostStatus;
|
||||
@@ -184,8 +184,6 @@ function renderNodeSvg(
|
||||
statusColor = useRealColors
|
||||
? "rgb(16,185,129)"
|
||||
: resolveCssVar("--accent-brand", "rgb(16,185,129)");
|
||||
} else if (status === "reachable") {
|
||||
statusColor = "rgb(251,191,36)";
|
||||
} else if (isOffline) {
|
||||
statusColor = useRealColors ? "rgb(239,68,68)" : "rgba(16,185,129,0.2)";
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user