fix: harden telemetry parsing and null node fallbacks

This commit is contained in:
SaelixCode
2026-03-18 20:31:57 -04:00
parent 7f23c88c73
commit f1f8e34da5
5 changed files with 53 additions and 2 deletions
+6
View File
@@ -41,6 +41,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **(Planned)** Port 2375 (TCP) fallback support; future releases may require SSH-only for Node config.
### Fixed
- Fixed backend MonitorService crash (`Cannot read properties of undefined (reading 'cpu_usage')`) occurring when Docker containers lacked CPU telemetry during transition states.
- Handled UI deleted nodes ghost API calls by intercepting 404 errors globally in API and forcing the UI to resync to the default Node context.
- Hardened `nodeContextMiddleware` in Express to intercept queries to invalid or deleted Node IDs gracefully instead of bubbling to Docker API 500 crashes.
- Hardened Remote Node connection testing (`docker.info()`) to explicitly validate expected Docker API daemon properties instead of merely checking string length.
- Caught Unhandled SFTP Promise Rejections in Node registry gracefully returning empty arrays to prevent frontend loading UI stalls.
- Fixed horizontal UI overflowing in Node Manager settings on smaller resolutions.
- **Fixed:** Docker API parsing bug where HTML string responses from misconfigured ports were counted as containers.
- **Fixed:** Stack list crashing when SFTP connections fail by gracefully catching SSH errors and returning empty arrays.
+10
View File
@@ -67,6 +67,16 @@ const nodeContextMiddleware = (req: Request, res: Response, next: NextFunction)
} else {
req.nodeId = NodeRegistry.getInstance().getDefaultNodeId();
}
// Intercept requests to deleted nodes to prevent downstream errors
if (req.path.startsWith('/api/') && !req.path.startsWith('/api/auth/')) {
const node = DatabaseService.getInstance().getNode(req.nodeId);
if (!node) {
res.status(404).json({ error: `Node with id ${req.nodeId} not found or was deleted.` });
return;
}
}
next();
};
+5 -1
View File
@@ -311,8 +311,10 @@ export class MonitorService {
private calculateCpuPercent(stats: any): number {
let cpuPercent = 0.0;
if (!stats?.cpu_stats?.cpu_usage || !stats?.precpu_stats?.cpu_usage) return 0.0;
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const systemDelta = (stats.cpu_stats.system_cpu_usage || 0) - (stats.precpu_stats.system_cpu_usage || 0);
const numCpus = stats.cpu_stats.online_cpus || (stats.cpu_stats.cpu_usage.percpu_usage ? stats.cpu_stats.cpu_usage.percpu_usage.length : 1);
if (systemDelta > 0.0 && cpuDelta > 0.0) {
@@ -322,6 +324,8 @@ 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) {
+19 -1
View File
@@ -48,6 +48,16 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
const updatedActive = data.find((n: Node) => n.id === activeNode.id);
if (updatedActive) {
setActiveNodeState(updatedActive);
} else {
// The active node was deleted! Fallback to default
const defaultNode = data.find((n: Node) => n.is_default);
if (defaultNode) {
setActiveNodeState(defaultNode);
} else if (data.length > 0) {
setActiveNodeState(data[0]);
} else {
setActiveNodeState(null);
}
}
}
}
@@ -65,7 +75,15 @@ export function NodeProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
refreshNodes();
}, []);
const handleNodeNotFound = () => {
console.warn('[NodeContext] Active node is unreachable or deleted. Forcing sync...');
refreshNodes();
};
window.addEventListener('node-not-found', handleNodeNotFound);
return () => window.removeEventListener('node-not-found', handleNodeNotFound);
}, [refreshNodes]);
return (
<NodeContext.Provider value={{ nodes, activeNode, setActiveNode, refreshNodes, isLoading }}>
+13
View File
@@ -24,6 +24,19 @@ export async function apiFetch(
throw new Error('Unauthorized');
}
// Intercept 404 Node Not Found responses and force context refresh
if (response.status === 404) {
try {
const clone = response.clone();
const errData = await clone.json();
if (errData.error && errData.error.includes('not found') && errData.error.includes('Node')) {
window.dispatchEvent(new Event('node-not-found'));
}
} catch (e) {
// Ignore JSON parse errors, caller handles standard 404s
}
}
return response;
}