feat: rework Electron desktop app to run standalone-first with optional two-way sync to a remote Termix server

This commit is contained in:
LukeGus
2026-07-21 01:27:03 -05:00
parent f44d09eef7
commit 08825c256d
77 changed files with 3658 additions and 929 deletions
+46 -4
View File
@@ -1,8 +1,31 @@
import axios, { type AxiosRequestConfig } from "axios";
import { handleApiError, statsApi } from "@/main-axios";
import {
handleApiError,
statsApi,
getRemoteStatsApi,
isElectron,
} from "@/main-axios";
import type { ServerMetrics, ServerStatus } from "@/main-axios";
import { getCachedServerStatuses } from "@/lib/hosts-request-cache";
// Metrics collection/viewer registration below (startMetricsPolling,
// registerMetricsViewer, etc.) is NOT origin-routed: the backend that
// receives the call must own the target host by numeric database id, and a
// synced host has a different numeric id in each database (only its
// syncId matches across them). Only the aggregate status read is merged
// across local + remote, same as tunnel status.
async function isRemoteSyncConnected(): Promise<boolean> {
if (!isElectron()) return false;
try {
const config = (await window.electronAPI?.invoke?.(
"get-remote-sync-config",
)) as { serverUrl?: string } | null;
return !!config?.serverUrl;
} catch {
return false;
}
}
type ApiConnectionLog = {
type: "info" | "success" | "warning" | "error";
stage: string;
@@ -76,6 +99,7 @@ export async function getAllServerStatuses(): Promise<
> {
return getCachedServerStatuses(async () => {
let lastError: unknown = null;
let localStatuses: Record<number, ServerStatus> = {};
for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) {
const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i];
@@ -89,7 +113,9 @@ export async function getAllServerStatuses(): Promise<
// blips don't look like real outages.
__silentRetry: !isFinalAttempt,
} as AxiosRequestConfig & { __silentRetry?: boolean });
return response.data || {};
localStatuses = response.data || {};
lastError = null;
break;
} catch (error) {
lastError = error;
if (!isTransientStatusError(error)) {
@@ -102,8 +128,24 @@ export async function getAllServerStatuses(): Promise<
}
}
handleApiError(lastError, "fetch server statuses");
return {};
if (lastError) {
handleApiError(lastError, "fetch server statuses");
return {};
}
if (await isRemoteSyncConnected()) {
try {
const remoteResult = await getRemoteStatsApi().get("/status", {
timeout: 8000,
__silentRetry: true,
} as AxiosRequestConfig & { __silentRetry?: boolean });
return { ...localStatuses, ...(remoteResult.data || {}) };
} catch {
// remote unreachable this tick -- fall back to local-only statuses
}
}
return localStatuses;
});
}