mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
fix: recover expired dashboard metrics sessions (#1355)
This commit is contained in:
@@ -256,10 +256,18 @@ export async function stopMetricsPolling(
|
|||||||
|
|
||||||
export async function sendMetricsHeartbeat(
|
export async function sendMetricsHeartbeat(
|
||||||
viewerSessionId: string,
|
viewerSessionId: string,
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await statsApi.post("/metrics/heartbeat", { viewerSessionId });
|
const response = await statsApi.post(
|
||||||
|
"/metrics/heartbeat",
|
||||||
|
{ viewerSessionId },
|
||||||
|
{ validateStatus: (status) => status === 200 || status === 404 },
|
||||||
|
);
|
||||||
|
return response.status !== 404;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
handleApiError(error, "send metrics heartbeat");
|
handleApiError(error, "send metrics heartbeat");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1366,7 +1366,10 @@ export function DashboardTab({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = newSessions.get(hostId);
|
const existing = newSessions.get(hostId);
|
||||||
if (!existing) {
|
if (existing && !(await sendMetricsHeartbeat(existing))) {
|
||||||
|
newSessions.delete(hostId);
|
||||||
|
}
|
||||||
|
if (!newSessions.has(hostId)) {
|
||||||
const reg = await registerMetricsViewer(hostId);
|
const reg = await registerMetricsViewer(hostId);
|
||||||
if (reg.skipped) return null;
|
if (reg.skipped) return null;
|
||||||
if (reg.success && reg.viewerSessionId) {
|
if (reg.success && reg.viewerSessionId) {
|
||||||
@@ -1489,8 +1492,14 @@ export function DashboardTab({
|
|||||||
if (!isVisible || viewerSessionsRef.current.size === 0) return;
|
if (!isVisible || viewerSessionsRef.current.size === 0) return;
|
||||||
const heartbeat = setInterval(async () => {
|
const heartbeat = setInterval(async () => {
|
||||||
if (document.visibilityState === "hidden") return;
|
if (document.visibilityState === "hidden") return;
|
||||||
for (const [, sessionId] of viewerSessionsRef.current) {
|
for (const [hostId, sessionId] of viewerSessionsRef.current) {
|
||||||
sendMetricsHeartbeat(sessionId).catch(() => {});
|
sendMetricsHeartbeat(sessionId)
|
||||||
|
.then((alive) => {
|
||||||
|
if (!alive && viewerSessionsRef.current.get(hostId) === sessionId) {
|
||||||
|
viewerSessionsRef.current.delete(hostId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
return () => clearInterval(heartbeat);
|
return () => clearInterval(heartbeat);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const statsApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
const statsApiMock = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() }));
|
||||||
const remoteStatsApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
const remoteStatsApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
||||||
const sshHostApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
const sshHostApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
||||||
const resolveConnectionOriginMock = vi.hoisted(() => vi.fn());
|
const resolveConnectionOriginMock = vi.hoisted(() => vi.fn());
|
||||||
@@ -24,6 +24,7 @@ vi.mock("@/lib/connection-origin", () => ({
|
|||||||
import {
|
import {
|
||||||
getAllServerStatuses,
|
getAllServerStatuses,
|
||||||
getServerMetricsById,
|
getServerMetricsById,
|
||||||
|
sendMetricsHeartbeat,
|
||||||
} from "../../api/host-metrics-status-api";
|
} from "../../api/host-metrics-status-api";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -98,3 +99,21 @@ describe("metrics request coalescing", () => {
|
|||||||
expect(statsApiMock.get).toHaveBeenCalledOnce();
|
expect(statsApiMock.get).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("metrics viewer heartbeat", () => {
|
||||||
|
it("reports a swept viewer session without throwing or logging", async () => {
|
||||||
|
statsApiMock.post.mockResolvedValueOnce({ status: 404 });
|
||||||
|
|
||||||
|
await expect(sendMetricsHeartbeat("expired-viewer")).resolves.toBe(false);
|
||||||
|
expect(statsApiMock.post).toHaveBeenCalledWith(
|
||||||
|
"/metrics/heartbeat",
|
||||||
|
{ viewerSessionId: "expired-viewer" },
|
||||||
|
{ validateStatus: expect.any(Function) },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a live viewer session", async () => {
|
||||||
|
statsApiMock.post.mockResolvedValueOnce({ status: 200 });
|
||||||
|
await expect(sendMetricsHeartbeat("live-viewer")).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user