chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

+4 -445
View File
@@ -646,12 +646,10 @@ function getApiUrl(path: string, defaultPort: number): string {
// PER-HOST ORIGIN ROUTING (Electron desktop only)
// ============================================================================
//
// hostApi/fileManagerApi above always point at the
// embedded local backend -- they're the shared, always-on instances. When a
// host's connection origin resolves to "remote" (see
// src/ui/lib/connection-origin.ts), the backend that actually holds that
// host's live SSH session is the connected remote server instead, so file
// manager and stats calls for that host must follow it there.
// hostApi above always points at the embedded local backend. When a host's
// connection origin resolves to "remote" (see src/ui/lib/connection-origin.ts),
// the backend that holds that host's live session is the connected remote
// server instead, so calls for that host must follow it there.
//
// These dynamically-baseURL'd instances resolve the remote server's URL and
// JWT fresh on every request (cheap, and correct even if the user
@@ -695,18 +693,8 @@ export function createRemoteOriginApiInstance(path: string): AxiosInstance {
return instance;
}
let remoteFileManagerApi: AxiosInstance | null = null;
let remoteCoreApi: AxiosInstance | null = null;
export function getRemoteFileManagerApi(): AxiosInstance {
if (!remoteFileManagerApi) {
remoteFileManagerApi = createRemoteOriginApiInstance(
"/plugin-api/file-manager",
);
}
return remoteFileManagerApi;
}
/**
* The remote server's core routes, unprefixed.
*
@@ -719,46 +707,11 @@ export function getRemoteCoreApi(): AxiosInstance {
return remoteCoreApi;
}
// Maps a live SSH session (keyed by sessionId, which today is the host's
// numeric id as a string -- see ensureSSHSessionForHost) to the resolved
// origin it was connected through, so every subsequent file-manager call
// for that session reaches the backend that actually holds it.
const sessionOrigins = new Map<string, "local" | "remote">();
export function setSessionOrigin(
sessionId: string,
origin: "local" | "remote",
): void {
sessionOrigins.set(sessionId, origin);
}
export function clearSessionOrigin(sessionId: string): void {
sessionOrigins.delete(sessionId);
}
export function getFileManagerApiForSession(sessionId: string): AxiosInstance {
return sessionOrigins.get(sessionId) === "remote"
? getRemoteFileManagerApi()
: fileManagerApi;
}
/** Which backend currently holds a live SSH session (see setSessionOrigin). */
export function getSessionOrigin(sessionId: string): "local" | "remote" {
return sessionOrigins.get(sessionId) === "remote" ? "remote" : "local";
}
function initializeApiInstances() {
// Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet
hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
sshHostApi = hostApi;
// File Manager Operations API - the file-manager plugin, mounted on the
// main backend.
fileManagerApi = createApiInstance(
getApiUrl("/plugin-api/file-manager", 30001),
"FILE_MANAGER",
);
// Authentication API (port 30001)
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
@@ -771,9 +724,6 @@ export let hostApi: AxiosInstance;
// Backward compatibility
export let sshHostApi: AxiosInstance;
// File Manager Operations API (file-manager plugin)
export let fileManagerApi: AxiosInstance;
// Authentication API (port 30001)
export let authApi: AxiosInstance;
@@ -949,391 +899,12 @@ export function handleApiError(error: unknown, operation: string): never {
// ============================================================================
// ============================================================================
// HOST-TO-HOST TRANSFER
// ============================================================================
export type TransferMethodPreference = "auto" | "tar" | "item_sftp";
export interface TransferScanSummary {
fileCount: number;
totalBytes: number;
largestFileBytes: number;
incompressibleRatio: number;
}
export interface TransferMethodPreview {
methodPreference: TransferMethodPreference;
resolvedMethod: "tar" | "item_sftp";
reasonKey: string;
sourcePlatform: "unix" | "windows";
destPlatform: "unix" | "windows";
sourceHasTar: boolean;
destHasTar: boolean;
summary: TransferScanSummary;
}
export async function getTransferMethodPreview(
sourceSessionId: string,
sourcePaths: string[],
destSessionId: string,
destPath: string,
methodPreference?: TransferMethodPreference,
): Promise<TransferMethodPreview> {
try {
const response = await fileManagerApi.post("/transferMethodPreview", {
sourceSessionId,
sourcePaths,
destSessionId,
destPath,
methodPreference: methodPreference ?? "auto",
});
return response.data;
} catch (error) {
handleApiError(error, "preview transfer method");
throw error;
}
}
export interface TransferHopMetrics {
id: string;
mbPerSec?: number;
}
export interface TransferTimings {
prepareDestMs?: number;
compressMs?: number;
transferMs?: number;
extractMs?: number;
verifyMs?: number;
directBenchmarkMs?: number;
relayBenchmarkMs?: number;
sourceDeleteMs?: number;
totalMs?: number;
transferBytes?: number;
endToEndMbPerSec?: number;
hops?: TransferHopMetrics[];
}
export function getTransferProgressPercent(
status: TransferProgressResponse,
): number | undefined {
if (
status.bytesTransferred !== undefined &&
status.totalBytes !== undefined &&
status.totalBytes > 0
) {
return Math.min(
100,
Math.round((status.bytesTransferred / status.totalBytes) * 100),
);
}
if (
status.itemsCompleted !== undefined &&
status.totalItems !== undefined &&
status.totalItems > 0
) {
return Math.min(
100,
Math.round((status.itemsCompleted / status.totalItems) * 100),
);
}
return undefined;
}
export function formatTransferMbPerSec(
mbPerSec?: number,
_bytes?: number,
_ms?: number,
): string {
if (mbPerSec === undefined || mbPerSec <= 0) return "";
if (mbPerSec < 1) return `${(mbPerSec * 1024).toFixed(0)} KB/s`;
return `${mbPerSec.toFixed(1)} MB/s`;
}
export function formatDurationMs(ms?: number): string {
if (ms === undefined) return "";
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.round((ms % 60000) / 1000);
return `${minutes}m ${seconds}s`;
}
interface TransferProgressTracker {
update(status: TransferProgressResponse): {
rate: number | undefined;
stalled: boolean;
};
}
export function createTransferProgressTracker(): TransferProgressTracker {
let lastBytes: number | undefined;
let lastTime: number | undefined;
let lastRate: number | undefined;
let lastActivityTime: number | undefined;
const STALL_THRESHOLD_MS = 5000;
return {
update(status) {
const now = Date.now();
const bytes = status.bytesTransferred;
if (
bytes !== undefined &&
lastBytes !== undefined &&
lastTime !== undefined
) {
const deltaBytes = bytes - lastBytes;
const deltaMs = now - lastTime;
if (deltaMs > 0 && deltaBytes >= 0) {
lastRate = (deltaBytes / deltaMs / 1024 / 1024) * 1000;
if (deltaBytes > 0) lastActivityTime = now;
}
} else if (bytes !== undefined) {
lastActivityTime = now;
}
lastBytes = bytes;
lastTime = now;
const stalled =
lastActivityTime !== undefined &&
now - lastActivityTime > STALL_THRESHOLD_MS &&
status.status === "running" &&
status.phase === "transferring";
return { rate: lastRate, stalled };
},
};
}
export interface TransferProgressResponse {
transferId: string;
status: "running" | "success" | "partial" | "error" | "cancelled";
phase:
| "compressing"
| "transferring"
| "benchmarking"
| "verifying"
| "extracting"
| "reconnecting";
bytesTransferred?: number;
totalBytes?: number;
itemsCompleted?: number;
totalItems?: number;
failedPaths?: string[];
message?: string;
method?: "stream" | "tar" | "item_sftp" | "direct_rsync";
sourcePaths?: string[];
destPath?: string;
sourceSessionId?: string;
destSessionId?: string;
startedAt?: number;
timings?: TransferTimings;
sourceDeleted?: boolean;
moveRequested?: boolean;
partialDestRemaining?: boolean;
cleanupCompleted?: boolean;
retryable?: boolean;
integrityVerified?: boolean;
parallelSegmentCount?: number;
}
export async function transferToHost(
sourceSessionId: string,
sourcePaths: string[],
destSessionId: string,
destPath: string,
move?: boolean,
methodPreference?: TransferMethodPreference,
parallelSegmentCount?: number,
): Promise<{ transferId: string }> {
try {
fileLogger.info("Starting host transfer", {
operation: "host_transfer",
sourceSessionId,
destSessionId,
sourcePaths,
destPath,
move,
methodPreference,
parallelSegmentCount,
});
const response = await fileManagerApi.post("/transferToHost", {
sourceSessionId,
sourcePaths,
destSessionId,
destPath,
move,
methodPreference: methodPreference ?? "auto",
parallelSegmentCount,
});
return response.data;
} catch (error) {
fileLogger.error("Failed to start host transfer", error, {
operation: "host_transfer",
sourceSessionId,
destSessionId,
sourcePaths,
});
handleApiError(error, "transfer to host");
throw error;
}
}
export async function getTransferStatus(
transferId: string,
): Promise<TransferProgressResponse> {
try {
const response = await fileManagerApi.get(`/transferStatus/${transferId}`);
return response.data;
} catch (error) {
handleApiError(error, "get transfer status");
throw error;
}
}
export async function listActiveTransfers(): Promise<{
transfers: TransferProgressResponse[];
}> {
try {
const response = await fileManagerApi.get("/activeTransfers");
return response.data;
} catch (error) {
handleApiError(error, "list active transfers");
throw error;
}
}
export async function cancelTransferToHost(transferId: string): Promise<void> {
try {
await fileManagerApi.post(`/transferCancel/${transferId}`);
} catch (error) {
fileLogger.warn("Transfer cancel request failed (non-fatal)", {
operation: "host_transfer",
transferId,
error,
});
}
}
export async function cleanupCancelledTransfer(
transferId: string,
): Promise<{ removedPaths: string[]; failedPaths: string[] }> {
try {
const response = await fileManagerApi.post(
`/transferCleanup/${transferId}`,
);
return response.data;
} catch (error) {
handleApiError(error, "clean up cancelled transfer");
throw error;
}
}
export async function retryTransferToHost(
transferId: string,
): Promise<{ ok: boolean; transferId: string }> {
try {
const response = await fileManagerApi.post(`/transferRetry/${transferId}`);
return response.data;
} catch (error) {
handleApiError(error, "retry transfer");
throw error;
}
}
export async function pollTransferUntilComplete(
transferId: string,
onProgress?: (status: TransferProgressResponse) => void,
intervalMs = 500,
): Promise<TransferProgressResponse> {
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const status = await getTransferStatus(transferId);
onProgress?.(status);
if (
status.status === "success" ||
status.status === "partial" ||
status.status === "error" ||
status.status === "cancelled"
) {
resolve(status);
return;
}
setTimeout(poll, intervalMs);
} catch (err) {
reject(err);
}
};
void poll();
});
}
// ============================================================================
// FILE MANAGER DATA
// ============================================================================
export interface TransferDestination {
id: number;
userId: string;
sourceHostId: number;
destHostId: number;
destPath: string;
destPathLabel?: string;
lastUsed?: string;
}
export async function getTransferRecent(
sourceHostId: number,
): Promise<TransferDestination[]> {
try {
const response = await authApi.get("/host/transfer/recent", {
params: { sourceHostId },
});
return response.data;
} catch (error) {
handleApiError(error, "get transfer recent destinations");
throw error;
}
}
export async function addTransferRecent(
sourceHostId: number,
destHostId: number,
destPath: string,
destPathLabel?: string,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/host/transfer/recent", {
sourceHostId,
destHostId,
destPath,
destPathLabel,
});
return response.data;
} catch (error) {
handleApiError(error, "add transfer recent destination");
throw error;
}
}
export {
getSSHHosts,
createSSHHost,
updateSSHHost,
bulkImportSSHHosts,
importSSHConfigHosts,
discoverProxmoxGuests,
discoverProxmoxGuestsStream,
syncProxmoxGuests,
bulkUpdateSSHHosts,
reorderSSHHosts,
deleteSSHHost,
@@ -1723,10 +1294,6 @@ export {
adminCreateUserCredential,
adminUpdateUserCredential,
adminDeleteUserCredential,
adminGetUserSnippets,
adminCreateUserSnippet,
adminUpdateUserSnippet,
adminDeleteUserSnippet,
} from "@/api/admin-user-data-api";
export {
@@ -1771,14 +1338,6 @@ export {
deployCredentialToHost,
} from "@/api/credentials-api";
// ============================================================================
// SNIPPETS API
// ============================================================================
// Snippets themselves are owned by the snippets plugin. This is the thin
// list read a few core pickers still need (see src/ui/api/snippets-api.ts).
export { getSnippets, executeSnippet } from "@/api/snippets-api";
// ============================================================================
export type { UptimeInfo, RecentActivityItem } from "@/api/dashboard-api";
export {