mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-27 12:18:59 +00:00
Merge pull request #42 from AnsoCode/fix/remote-ws-stats-exec-openapp
fix(remote): repair stats, bash exec, and Open App for remote nodes
This commit is contained in:
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- **Fixed:** Container stats (CPU/RAM/NET), bash exec terminal, and "Open App" button all broken for remote nodes — stats and exec WebSockets connected to the bare root (`ws://host`) with no `?nodeId=` query param, so the upgrade handler couldn't detect the remote node and skipped the WS proxy. Moved all generic WebSockets to `/ws?nodeId=` path; upgrade handler now correctly proxies them to the remote Sencho instance.
|
||||
- **Fixed:** Bash exec and stats WebSockets not reaching the backend in `npm run dev` — Vite proxy config now includes `ws: true` on `/api` and a new `/ws` proxy entry so all WebSocket upgrades are forwarded to `localhost:3000`.
|
||||
- **Fixed:** Backend WS message handler crashing when a proxied WebSocket arrives from a gateway and the forwarded `nodeId` doesn't exist in the remote instance's DB — now falls back to the default local node instead of throwing.
|
||||
- **Fixed:** "Open App" button opening `http://localhost:{port}` for remote node containers — now resolves the hostname from the remote node's `api_url` so the correct remote host is used.
|
||||
- **Fixed:** Remote node system stats, container stats, logs, and exec returning errors — `remoteNodeProxy` middleware was already positioned before API route definitions, but `/api/system/stats` contained a dead remote branch that called `NodeRegistry.getDocker()` for remote nodes, which throws since remote nodes have no direct Docker socket access. Removed the broken branch; remote requests are correctly intercepted by the proxy middleware before reaching any route handler.
|
||||
- **Added:** Background image update checker — `ImageUpdateService` polls OCI-compliant registries (Docker Hub, GHCR, LSCR, etc.) every 6 hours using manifest digest comparison against local `RepoDigests`. Results cached in a new `stack_update_status` SQLite table. A pulsing blue dot badge appears in the stack list sidebar for stacks with available updates. Manual refresh available via `POST /api/image-updates/refresh` (rate-limited to once per 10 minutes).
|
||||
- **Fixed:** `AppStoreView` and `GlobalObservabilityView` using raw `fetch()` instead of `apiFetch()` — all calls now inject the `x-node-id` header so templates, deploys, stacks, and logs are correctly proxied to the active remote node.
|
||||
|
||||
+10
-6
@@ -513,15 +513,19 @@ wss.on('connection', (ws) => {
|
||||
if (data.action === 'connectTerminal') {
|
||||
terminalWs = ws;
|
||||
} else if (data.action === 'streamStats') {
|
||||
const nodeId = data.nodeId ? parseInt(data.nodeId, 10) : NodeRegistry.getInstance().getDefaultNodeId();
|
||||
const dockerController = DockerController.getInstance(nodeId);
|
||||
dockerController.streamStats(data.containerId, ws);
|
||||
const requestedId = data.nodeId ? parseInt(data.nodeId, 10) : NodeRegistry.getInstance().getDefaultNodeId();
|
||||
// When a WS is proxied from a gateway to this remote instance, the nodeId in the
|
||||
// message belongs to the gateway's DB and won't resolve locally. Fall back to local.
|
||||
let nodeId = requestedId;
|
||||
try { NodeRegistry.getInstance().getDocker(requestedId); } catch { nodeId = NodeRegistry.getInstance().getDefaultNodeId(); }
|
||||
DockerController.getInstance(nodeId).streamStats(data.containerId, ws);
|
||||
} else if (data.action === 'execContainer') {
|
||||
// Handle container exec for bash access
|
||||
// Input, resize, and cleanup are handled inside execContainer's closure
|
||||
const nodeId = data.nodeId ? parseInt(data.nodeId, 10) : NodeRegistry.getInstance().getDefaultNodeId();
|
||||
const dockerController = DockerController.getInstance(nodeId);
|
||||
dockerController.execContainer(data.containerId, ws);
|
||||
const requestedId = data.nodeId ? parseInt(data.nodeId, 10) : NodeRegistry.getInstance().getDefaultNodeId();
|
||||
let nodeId = requestedId;
|
||||
try { NodeRegistry.getInstance().getDocker(requestedId); } catch { nodeId = NodeRegistry.getInstance().getDefaultNodeId(); }
|
||||
DockerController.getInstance(nodeId).execContainer(data.containerId, ws);
|
||||
}
|
||||
} catch (error) {
|
||||
// Malformed JSON - ignore silently
|
||||
|
||||
@@ -114,7 +114,8 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
|
||||
|
||||
// Connect to WebSocket for bash exec
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(`${wsProtocol}//${window.location.host}`);
|
||||
const activeNodeId = localStorage.getItem('sencho-active-node') || '';
|
||||
const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws${activeNodeId ? `?nodeId=${activeNodeId}` : ''}`);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
|
||||
@@ -225,12 +225,13 @@ export default function EditorLayout() {
|
||||
if (!container?.Id) return;
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(`${wsProtocol}//${window.location.host}`);
|
||||
const activeNodeId = localStorage.getItem('sencho-active-node') || '';
|
||||
const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws${activeNodeId ? `?nodeId=${activeNodeId}` : ''}`);
|
||||
wsMap[container.Id] = ws;
|
||||
ws.onopen = () => ws.send(JSON.stringify({
|
||||
action: 'streamStats',
|
||||
containerId: container.Id,
|
||||
nodeId: localStorage.getItem('sencho-active-node') || undefined
|
||||
nodeId: activeNodeId || undefined
|
||||
}));
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
@@ -1099,7 +1100,12 @@ export default function EditorLayout() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg h-8 w-8"
|
||||
onClick={() => window.open(`http://${window.location.hostname}:${mainPort}`, '_blank')}
|
||||
onClick={() => {
|
||||
const host = activeNode?.type === 'remote' && activeNode?.api_url
|
||||
? new URL(activeNode.api_url).hostname
|
||||
: window.location.hostname;
|
||||
window.open(`http://${host}:${mainPort}`, '_blank');
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -16,6 +16,12 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
'/ws': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user