mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +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(
|
||||
viewerSessionId: string,
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
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) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
return false;
|
||||
}
|
||||
handleApiError(error, "send metrics heartbeat");
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1366,7 +1366,10 @@ export function DashboardTab({
|
||||
|
||||
try {
|
||||
const existing = newSessions.get(hostId);
|
||||
if (!existing) {
|
||||
if (existing && !(await sendMetricsHeartbeat(existing))) {
|
||||
newSessions.delete(hostId);
|
||||
}
|
||||
if (!newSessions.has(hostId)) {
|
||||
const reg = await registerMetricsViewer(hostId);
|
||||
if (reg.skipped) return null;
|
||||
if (reg.success && reg.viewerSessionId) {
|
||||
@@ -1489,8 +1492,14 @@ export function DashboardTab({
|
||||
if (!isVisible || viewerSessionsRef.current.size === 0) return;
|
||||
const heartbeat = setInterval(async () => {
|
||||
if (document.visibilityState === "hidden") return;
|
||||
for (const [, sessionId] of viewerSessionsRef.current) {
|
||||
sendMetricsHeartbeat(sessionId).catch(() => {});
|
||||
for (const [hostId, sessionId] of viewerSessionsRef.current) {
|
||||
sendMetricsHeartbeat(sessionId)
|
||||
.then((alive) => {
|
||||
if (!alive && viewerSessionsRef.current.get(hostId) === sessionId) {
|
||||
viewerSessionsRef.current.delete(hostId);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}, 30000);
|
||||
return () => clearInterval(heartbeat);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 sshHostApiMock = vi.hoisted(() => ({ get: vi.fn() }));
|
||||
const resolveConnectionOriginMock = vi.hoisted(() => vi.fn());
|
||||
@@ -24,6 +24,7 @@ vi.mock("@/lib/connection-origin", () => ({
|
||||
import {
|
||||
getAllServerStatuses,
|
||||
getServerMetricsById,
|
||||
sendMetricsHeartbeat,
|
||||
} from "../../api/host-metrics-status-api";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -98,3 +99,21 @@ describe("metrics request coalescing", () => {
|
||||
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