fix: count plugin ssh logins toward host status and stop showing reachable as a warning

This commit is contained in:
LukeGus committed 2026-09-30 17:49:09 -05:00
1 parent 69b1b9805b
commit f97f35de1f
12 files changed
+143 -25

No files matched your search

+28 -12
View File
@@ -3,9 +3,11 @@
*
* Every host with status checks on gets a TCP connection to its port on a
* timer. That alone says "reachable". A host becomes "online" when a login
* worked: an open terminal session, or a plugin reporting one through
* ctx.hosts.status.reportLogin. Probing never logs in: on RADIUS or Duo backed
* devices a login fires a real 2FA push, every interval.
* worked: an open terminal session, any plugin connection through ctx.ssh, or
* a plugin reporting one through ctx.hosts.status.reportLogin. It stays online
* for LOGIN_FRESH_MS after the last login while the port still answers.
* Probing never logs in: on RADIUS or Duo backed devices a login fires a real
* 2FA push, every interval.
*
* Polling is demand driven. A user asking for statuses starts polling their
* own hosts; nothing is probed for a user who never opened the app.
@@ -31,6 +33,8 @@ export const GLOBAL_STATUS_INTERVAL_KEY = "global_status_check_interval";
export const DEFAULT_STATUS_INTERVAL = 60;
/** A status younger than this is fresh enough for check(). */
const FRESH_MS = 30_000;
/** How long a successful login keeps a reachable host online. */
export const LOGIN_FRESH_MS = 60 * 60_000;
export interface HostStatusEntry {
status: HostStatus;
@@ -77,6 +81,7 @@ export interface HostStatusDeps {
) => Promise<boolean>;
globalInterval: () => number;
emit: (payload: HostStatusPayload) => void;
now?: () => number;
}
function parseJumpHosts(raw: string | null): Array<{ hostId: number }> {
@@ -154,7 +159,7 @@ export class HostStatusService {
private readonly owners = new Map<number, string>();
private readonly inFlight = new Map<number, Promise<void>>();
private readonly sessionOnline = new Set<number>();
private readonly loggedIn = new Set<number>();
private readonly lastLogin = new Map<number, number>();
private readonly startedUsers = new Set<string>();
private readonly portResolvers = new Map<string, Set<PortResolver>>();
private readonly limiter = new ConcurrentLimiter(20);
@@ -247,11 +252,11 @@ export class HostStatusService {
outcome: { ok: boolean; hostKeyChanged?: boolean },
): void {
if (outcome.ok) {
this.loggedIn.add(hostId);
this.lastLogin.set(hostId, this.now());
this.set(hostId, { status: statusAfterAuthentication(true) });
return;
}
this.loggedIn.delete(hostId);
this.lastLogin.delete(hostId);
this.set(hostId, {
status: this.sessionOnline.has(hostId)
? "online"
@@ -346,7 +351,7 @@ export class HostStatusService {
this.stopPolling(hostId);
this.store.delete(hostId);
this.owners.delete(hostId);
this.loggedIn.delete(hostId);
this.lastLogin.delete(hostId);
}
private clearHostKeyReason(hostId: number): void {
@@ -361,18 +366,29 @@ export class HostStatusService {
private setSessionOnline(hostId: number, online: boolean): void {
if (online) {
this.sessionOnline.add(hostId);
this.lastLogin.set(hostId, this.now());
if (this.polled.has(hostId)) this.set(hostId, { status: "online" });
return;
}
this.sessionOnline.delete(hostId);
if (
!this.loggedIn.has(hostId) &&
!this.isLoggedIn(hostId) &&
this.store.get(hostId)?.status === "online"
) {
this.set(hostId, { status: "reachable" });
}
}
private now(): number {
return this.deps.now?.() ?? Date.now();
}
private isLoggedIn(hostId: number): boolean {
if (this.sessionOnline.has(hostId)) return true;
const at = this.lastLogin.get(hostId);
return at !== undefined && this.now() - at < LOGIN_FRESH_MS;
}
private intervalMs(target: StatusTarget): number {
const seconds = target.statusCheckInterval ?? this.deps.globalInterval();
const intervalMs = Math.max(5, seconds) * 1000;
@@ -452,10 +468,10 @@ export class HostStatusService {
const current = this.store.get(target.id);
this.set(target.id, {
status:
reachable && this.sessionOnline.has(target.id)
? "online"
: statusAfterReachabilityCheck(reachable, current?.status),
status: statusAfterReachabilityCheck(
reachable,
this.isLoggedIn(target.id),
),
...(reachable && current?.reason === "host_key_changed"
? { reason: "host_key_changed" as const }
: {}),
+2 -2
View File
@@ -10,10 +10,10 @@ export function isHostKeyVerificationError(error: unknown): boolean {
export function statusAfterReachabilityCheck(
reachable: boolean,
current?: HostStatus,
loggedIn: boolean,
): HostStatus {
if (!reachable) return "offline";
return current === "online" ? "online" : "reachable";
return loggedIn ? "online" : "reachable";
}
export function statusAfterAuthentication(