mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix: harden telemetry parsing and null node fallbacks
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user