mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-28 20:59:09 +00:00
fix: Distributed API UI & metrics polish + DEP0060 suppression
Add Node modal — type selector & state reset:
- Restored a Local/Remote <Select> dropdown in renderFormFields so users can
explicitly choose the node type instead of it defaulting silently to 'remote'.
- Switching type clears api_url and api_token so no stale remote credentials
carry over if a user switches from Remote to Local mid-form.
- Replaced the static "Add Remote Node" title with a dynamic one that reflects
the currently selected type ("Add Local Node" / "Add Remote Node").
- onOpenChange now resets formData to defaultFormData whenever the dialog
opens, preventing stale values from a previous session leaking in.
Remote connection details — real metrics:
- testRemoteConnection previously returned hard-coded '-' for containers,
images, and cpus after a successful auth/check ping.
- Now fires three parallel requests (Promise.allSettled) after auth passes:
/api/stats → containers total + running count
/api/system/stats → cpu.cores
/api/system/images → image list length
- Each field falls back to '-' gracefully if an endpoint is unavailable,
so a slow or older remote instance never breaks the connection test.
DEP0060 util._extend suppression:
- http-proxy@1.18.1 calls util._extend when createProxyServer() is first
invoked at runtime (NOT at import time). A process.emitWarning override
placed before the proxy instantiations intercepts only DEP0060 without
suppressing any other warnings. No package version changes needed.
Also includes linter/formatter normalisation across multiple files.
This commit is contained in:
@@ -6,7 +6,7 @@ import { LogFormatter } from './LogFormatter';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
* ComposeService — local docker compose CLI execution.
|
||||
* ComposeService - local docker compose CLI execution.
|
||||
*
|
||||
* In the Distributed API model, remote node compose operations are handled
|
||||
* by the remote Sencho instance. This service only executes commands locally.
|
||||
@@ -160,7 +160,7 @@ export class ComposeService {
|
||||
let localProcesses: ReturnType<typeof spawn>[] = [];
|
||||
|
||||
const onWsClose = () => {
|
||||
localProcesses.forEach(cp => { try { cp.kill(); } catch {} });
|
||||
localProcesses.forEach(cp => { try { cp.kill(); } catch { } });
|
||||
};
|
||||
|
||||
ws.on('close', onWsClose);
|
||||
|
||||
@@ -350,7 +350,7 @@ class DockerController {
|
||||
await container.start();
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 304) {
|
||||
// Container already running — not an error
|
||||
// Container already running - not an error
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
@@ -364,7 +364,7 @@ class DockerController {
|
||||
await container.stop();
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 304) {
|
||||
// Container already stopped — not an error
|
||||
// Container already stopped - not an error
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
@@ -445,7 +445,7 @@ class DockerController {
|
||||
|
||||
/**
|
||||
* Exec into a container with full session isolation.
|
||||
* All state (exec instance, stream) lives in this closure — no singleton traps.
|
||||
* All state (exec instance, stream) lives in this closure - no singleton traps.
|
||||
* The WebSocket message handler is registered here to handle input, resize, and cleanup.
|
||||
*/
|
||||
public async execContainer(containerId: string, ws: WebSocket) {
|
||||
@@ -516,7 +516,7 @@ class DockerController {
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON or malformed message — ignore
|
||||
// Non-JSON or malformed message - ignore
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { promises as fsPromises } from 'fs';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
* FileSystemService — local-only file I/O for compose stack management.
|
||||
* FileSystemService - local-only file I/O for compose stack management.
|
||||
*
|
||||
* In the Distributed API model, remote node file operations are handled
|
||||
* by the remote Sencho instance itself. This service only operates on
|
||||
|
||||
@@ -121,7 +121,7 @@ export class MonitorService {
|
||||
const nodes = DatabaseService.getInstance().getNodes();
|
||||
for (const node of nodes) {
|
||||
if (!node.id) continue;
|
||||
// Remote nodes run their own MonitorService locally — skip direct Docker access
|
||||
// Remote nodes run their own MonitorService locally - skip direct Docker access
|
||||
if (node.type === 'remote') continue;
|
||||
try {
|
||||
const docker = DockerController.getInstance(node.id);
|
||||
@@ -208,7 +208,7 @@ export class MonitorService {
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node.id) continue;
|
||||
// Remote nodes are self-monitoring — skip direct Docker access
|
||||
// Remote nodes are self-monitoring - skip direct Docker access
|
||||
if (node.type === 'remote') continue;
|
||||
try {
|
||||
const docker = DockerController.getInstance(node.id);
|
||||
@@ -329,7 +329,7 @@ export class MonitorService {
|
||||
|
||||
private calculateMemoryPercent(stats: any): number {
|
||||
if (!stats?.memory_stats?.usage || !stats?.memory_stats?.limit) return 0.0;
|
||||
|
||||
|
||||
const used_memory = stats.memory_stats.usage - (stats.memory_stats.stats?.cache || 0);
|
||||
const available_memory = stats.memory_stats.limit;
|
||||
if (available_memory > 0) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export class NodeRegistry {
|
||||
private static instance: NodeRegistry;
|
||||
private connections: Map<number, Docker> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
public static getInstance(): NodeRegistry {
|
||||
if (!NodeRegistry.instance) {
|
||||
@@ -25,7 +25,7 @@ export class NodeRegistry {
|
||||
|
||||
/**
|
||||
* Get a Docker client for a LOCAL node only.
|
||||
* Remote nodes are never accessed via Dockerode — use the HTTP proxy instead.
|
||||
* Remote nodes are never accessed via Dockerode - use the HTTP proxy instead.
|
||||
*/
|
||||
public getDocker(nodeId: number): Docker {
|
||||
if (this.connections.has(nodeId)) {
|
||||
@@ -42,7 +42,7 @@ export class NodeRegistry {
|
||||
if (node.type === 'remote') {
|
||||
throw new Error(
|
||||
`Node "${node.name}" is a remote Distributed API node. ` +
|
||||
`Its Docker daemon is not directly accessible — all requests are proxied via HTTP.`
|
||||
`Its Docker daemon is not directly accessible - all requests are proxied via HTTP.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,36 +153,46 @@ export class NodeRegistry {
|
||||
return { success: false, error: 'Remote node is missing an API URL or token. Configure it in Settings → Nodes.' };
|
||||
}
|
||||
|
||||
const baseUrl = node.api_url.replace(/\/$/, '');
|
||||
const headers = { Authorization: `Bearer ${node.api_token}` };
|
||||
|
||||
try {
|
||||
const baseUrl = node.api_url.replace(/\/$/, '');
|
||||
const response = await axios.get(`${baseUrl}/api/auth/check`, {
|
||||
headers: { Authorization: `Bearer ${node.api_token}` },
|
||||
timeout: 8000,
|
||||
});
|
||||
// Step 1: Verify auth. A 401 here means wrong token — surface that clearly.
|
||||
const authRes = await axios.get(`${baseUrl}/api/auth/check`, { headers, timeout: 8000 });
|
||||
if (authRes.status !== 200) throw new Error(`Unexpected status ${authRes.status}`);
|
||||
|
||||
if (response.status === 200) {
|
||||
db.updateNodeStatus(node.id, 'online');
|
||||
return {
|
||||
success: true,
|
||||
info: {
|
||||
name: node.name,
|
||||
serverVersion: 'Remote Sencho',
|
||||
os: 'Remote',
|
||||
architecture: 'Remote',
|
||||
containers: '—',
|
||||
containersRunning: '—',
|
||||
images: '—',
|
||||
memTotal: 0,
|
||||
cpus: '—',
|
||||
}
|
||||
};
|
||||
}
|
||||
db.updateNodeStatus(node.id, 'online');
|
||||
|
||||
throw new Error(`Unexpected status ${response.status}`);
|
||||
// Step 2: Fetch Docker stats in parallel. Use allSettled so a slow or missing
|
||||
// endpoint doesn't fail the whole test — each field falls back to '-' gracefully.
|
||||
const [statsResult, sysResult, imagesResult] = await Promise.allSettled([
|
||||
axios.get(`${baseUrl}/api/stats`, { headers, timeout: 8000 }),
|
||||
axios.get(`${baseUrl}/api/system/stats`, { headers, timeout: 8000 }),
|
||||
axios.get(`${baseUrl}/api/system/images`, { headers, timeout: 8000 }),
|
||||
]);
|
||||
|
||||
const stats = statsResult.status === 'fulfilled' ? statsResult.value.data : null;
|
||||
const sys = sysResult.status === 'fulfilled' ? sysResult.value.data : null;
|
||||
const images = imagesResult.status === 'fulfilled' ? imagesResult.value.data : null;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
info: {
|
||||
name: node.name,
|
||||
serverVersion: 'Remote Sencho',
|
||||
os: 'Remote',
|
||||
architecture: 'Remote',
|
||||
containers: stats?.total ?? '-',
|
||||
containersRunning: stats?.active ?? '-',
|
||||
images: Array.isArray(images) ? images.length : '-',
|
||||
memTotal: sys?.memory?.total ?? 0,
|
||||
cpus: sys?.cpu?.cores ?? '-',
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
db.updateNodeStatus(node.id, 'offline');
|
||||
const msg = error.response?.status === 401
|
||||
? 'Authentication failed — check the API token.'
|
||||
? 'Authentication failed - check the API token.'
|
||||
: (error.message || 'Connection failed');
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user