mirror of
https://github.com/RGJorge/ContainerFlow.git
synced 2026-08-03 07:21:42 +02:00
v0.0.9
This commit is contained in:
+47
-54
@@ -269,61 +269,52 @@ function Dashboard({ token }: { token: string }) {
|
|||||||
initialLayoutDone.current = true;
|
initialLayoutDone.current = true;
|
||||||
} else {
|
} else {
|
||||||
setNodes((prev) => {
|
setNodes((prev) => {
|
||||||
const updated = prev.map((n) => {
|
const newNodeMap = new Map(newNodes.map((n) => [n.id, n]));
|
||||||
const u = newNodes.find((nn) => nn.id === n.id);
|
const prevNodeMap = new Map(prev.map((n) => [n.id, n]));
|
||||||
if (!u) return null;
|
|
||||||
return { ...n, data: u.data };
|
|
||||||
}).filter(Boolean) as Node[];
|
|
||||||
|
|
||||||
const existingIds = new Set(updated.map((n) => n.id));
|
// 1. Update existing nodes (keep position, update data)
|
||||||
const brand = newNodes.filter((n) => !existingIds.has(n.id));
|
const result: Node[] = [];
|
||||||
|
for (const nn of newNodes) {
|
||||||
|
const existing = prevNodeMap.get(nn.id);
|
||||||
|
if (existing) {
|
||||||
|
// Keep position and style, update data
|
||||||
|
result.push({ ...existing, data: nn.data });
|
||||||
|
} else {
|
||||||
|
// New node — use saved position if available
|
||||||
|
const saved = savedPositions.current[nn.id];
|
||||||
|
result.push(saved ? { ...nn, position: saved } : nn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nodes in prev but NOT in newNodes are simply dropped (they disappeared)
|
||||||
|
|
||||||
if (brand.length === 0) return updated;
|
// 2. Resize groups to fit their children
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
const hasSaved = brand.some((n) => savedPositions.current[n.id]);
|
const n = result[i];
|
||||||
if (hasSaved) {
|
if (n.type !== "group") continue;
|
||||||
let positioned = brand.map((n) => {
|
const kids = result.filter((c) => c.parentId === n.id);
|
||||||
const saved = savedPositions.current[n.id];
|
if (kids.length === 0) continue;
|
||||||
if (saved) return { ...n, position: saved };
|
let maxRight = 0;
|
||||||
return n;
|
let maxBottom = 0;
|
||||||
});
|
for (const k of kids) {
|
||||||
positioned = positioned.map((n) => {
|
maxRight = Math.max(maxRight, k.position.x + NODE_W + G_PAD);
|
||||||
if (n.type !== "group") return n;
|
maxBottom = Math.max(maxBottom, k.position.y + NODE_H + G_PAD);
|
||||||
const kids = [...updated, ...positioned].filter((c) => c.parentId === n.id);
|
}
|
||||||
if (kids.length === 0) return n;
|
const minW = NODE_W + G_PAD * 3;
|
||||||
let maxRight = 0;
|
const newW = Math.max(maxRight, minW);
|
||||||
let maxBottom = 0;
|
const newH = Math.max(maxBottom, MIN_Y + NODE_H + G_PAD);
|
||||||
for (const k of kids) {
|
result[i] = { ...n, style: { ...n.style, width: newW, height: newH } };
|
||||||
maxRight = Math.max(maxRight, k.position.x + NODE_W + G_PAD);
|
|
||||||
maxBottom = Math.max(maxBottom, k.position.y + NODE_H + G_PAD);
|
|
||||||
}
|
|
||||||
const minW = NODE_W + G_PAD * 3;
|
|
||||||
const newW = Math.max(maxRight, minW);
|
|
||||||
const newH = Math.max(maxBottom, MIN_Y + NODE_H + G_PAD);
|
|
||||||
return { ...n, style: { ...n.style, width: newW, height: newH } };
|
|
||||||
});
|
|
||||||
return [...updated, ...positioned];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxRightX = 0;
|
// 3. Recompute edges
|
||||||
for (const n of updated) {
|
const { edges: updatedEdges, activeHandles } = computeEdges(result, filteredConnections);
|
||||||
if (n.type === "group") {
|
setEdges(updatedEdges);
|
||||||
const w = (n.style?.width as number) || NODE_W + G_PAD * 3;
|
for (const n of result) {
|
||||||
maxRightX = Math.max(maxRightX, n.position.x + w);
|
if (n.type === "service") {
|
||||||
|
(n.data as any).activeHandles = activeHandles.get(n.id) || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGroups = brand.filter((n) => n.type === "group");
|
return result;
|
||||||
const offsetX = maxRightX > 0 ? maxRightX + 50 - (newGroups[0]?.position.x || 0) : 0;
|
|
||||||
|
|
||||||
const positioned = brand.map((n) => {
|
|
||||||
if (n.type === "group" && offsetX > 0) {
|
|
||||||
return { ...n, position: { x: n.position.x + offsetX, y: n.position.y } };
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...updated, ...positioned];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [filteredServices, filteredConnections, statsVersion]);
|
}, [filteredServices, filteredConnections, statsVersion]);
|
||||||
@@ -507,22 +498,24 @@ function Dashboard({ token }: { token: string }) {
|
|||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{ padding: 0.3 }}
|
fitViewOptions={{ padding: 0.3 }}
|
||||||
minZoom={0.2}
|
minZoom={0.3}
|
||||||
maxZoom={2.5}
|
maxZoom={1}
|
||||||
panOnScroll={true}
|
panOnScroll={true}
|
||||||
|
translateExtent={[[-1000, -1000], [8000, 6000]]}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
>
|
>
|
||||||
<ParticleOverlay engine={engine} settings={flowSettings} onNodeHits={handleNodeHits} />
|
<ParticleOverlay engine={engine} settings={flowSettings} onNodeHits={handleNodeHits} />
|
||||||
<Background color="#1e293b" gap={24} size={1} />
|
<Background color="#374151" gap={30} size={2} />
|
||||||
<Controls position="bottom-left" />
|
<Controls position="bottom-left" />
|
||||||
<EdgeLegend />
|
<EdgeLegend />
|
||||||
<MiniMap
|
<MiniMap
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
nodeColor={(n) => {
|
nodeColor={(n) => {
|
||||||
|
if (n.type === "group") return "#1e293b";
|
||||||
const state = (n.data as any)?.state;
|
const state = (n.data as any)?.state;
|
||||||
if (state === "running") return "#22c55e";
|
if (state === "running") return "#22c55e80";
|
||||||
if (state === "exited" || state === "dead") return "#ef4444";
|
if (state === "exited" || state === "dead") return "#ef444480";
|
||||||
return "#f59e0b";
|
return "#f59e0b80";
|
||||||
}}
|
}}
|
||||||
style={{ background: "#0f172a" }}
|
style={{ background: "#0f172a" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ export function useDocker(token = "") {
|
|||||||
break;
|
break;
|
||||||
case "connections":
|
case "connections":
|
||||||
setConnections((prev) => {
|
setConnections((prev) => {
|
||||||
if (prev.length === msg.data.length) return prev;
|
if (prev.length === msg.data.length &&
|
||||||
|
prev.every((c: any, i: number) => c.from === msg.data[i].from && c.to === msg.data[i].to)) return prev;
|
||||||
return msg.data;
|
return msg.data;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
+31
-12
@@ -15,6 +15,7 @@
|
|||||||
background-color: #0f172a !important;
|
background-color: #0f172a !important;
|
||||||
border: 1px solid #1e293b !important;
|
border: 1px solid #1e293b !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__controls {
|
.react-flow__controls {
|
||||||
@@ -27,6 +28,13 @@
|
|||||||
background-color: #1e293b !important;
|
background-color: #1e293b !important;
|
||||||
color: #94a3b8 !important;
|
color: #94a3b8 !important;
|
||||||
border-bottom: 1px solid #334155 !important;
|
border-bottom: 1px solid #334155 !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button svg {
|
||||||
|
width: 14px !important;
|
||||||
|
height: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__controls-button:hover {
|
.react-flow__controls-button:hover {
|
||||||
@@ -34,24 +42,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Flash animations for Docker events */
|
/* Flash animations for Docker events */
|
||||||
@keyframes flash-green {
|
@keyframes flash-border-green {
|
||||||
0%, 100% { box-shadow: 0 0 0 0 transparent; }
|
0% { border-color: #22c55e; box-shadow: 0 0 12px 2px rgba(34, 197, 94, 0.4); }
|
||||||
50% { box-shadow: 0 0 20px 4px rgba(34, 197, 94, 0.6); }
|
100% { border-color: rgba(51, 65, 85, 0.8); box-shadow: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash-red {
|
@keyframes flash-border-red {
|
||||||
0%, 100% { box-shadow: 0 0 0 0 transparent; }
|
0% { border-color: #ef4444; box-shadow: 0 0 12px 2px rgba(239, 68, 68, 0.4); }
|
||||||
50% { box-shadow: 0 0 20px 4px rgba(239, 68, 68, 0.6); }
|
100% { border-color: #ef4444; box-shadow: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash-yellow {
|
@keyframes flash-border-yellow {
|
||||||
0%, 100% { box-shadow: 0 0 0 0 transparent; }
|
0% { border-color: #f59e0b; box-shadow: 0 0 12px 2px rgba(245, 158, 11, 0.4); }
|
||||||
50% { box-shadow: 0 0 20px 4px rgba(245, 158, 11, 0.6); }
|
100% { border-color: rgba(51, 65, 85, 0.8); box-shadow: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash-start { animation: flash-green 0.6s ease-out; }
|
.flash-start { animation: flash-border-green 1.2s ease-out; }
|
||||||
.flash-stop { animation: flash-red 0.6s ease-out; }
|
.flash-stop { animation: flash-border-red 1.2s ease-out forwards; }
|
||||||
.flash-restart { animation: flash-yellow 0.6s ease-out 2; }
|
.flash-restart { animation: flash-border-yellow 1.2s ease-out 2; }
|
||||||
|
|
||||||
|
/* Fade out animation for removed nodes */
|
||||||
|
@keyframes node-fade-out {
|
||||||
|
0% { opacity: 1; transform: scale(1); }
|
||||||
|
100% { opacity: 0; transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-removing {
|
||||||
|
animation: node-fade-out 0.8s ease-out forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Log panel slide-up */
|
/* Log panel slide-up */
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ interface ServiceNodeData {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateStyles: Record<string, { ring: string; dot: string; bg: string }> = {
|
const stateStyles: Record<string, { ring: string; dot: string; bg: string; border: string }> = {
|
||||||
running: { ring: "ring-emerald-500/50", dot: "bg-emerald-500", bg: "bg-emerald-500/10" },
|
running: { ring: "ring-emerald-500/50", dot: "bg-emerald-500", bg: "bg-emerald-500/10", border: "border-slate-700/80" },
|
||||||
exited: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10" },
|
exited: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10", border: "border-red-500/60" },
|
||||||
paused: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10" },
|
paused: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/60" },
|
||||||
restarting: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10" },
|
restarting: { ring: "ring-amber-500/50", dot: "bg-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/60" },
|
||||||
dead: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10" },
|
dead: { ring: "ring-red-500/50", dot: "bg-red-500", bg: "bg-red-500/10", border: "border-red-500/60" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map image/name patterns to Lucide icons and colors
|
// Map image/name patterns to Lucide icons and colors
|
||||||
@@ -123,7 +123,7 @@ export const ServiceNode = memo(function ServiceNode({ data }: NodeProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
title={`${d.label} (${d.state})\nImage: ${d.image}\nID: ${d.id || ""}\nPorts: ${d.ports?.map((p) => `${p.host}:${p.container}`).join(", ") || "none"}`}
|
title={`${d.label} (${d.state})\nImage: ${d.image}\nID: ${d.id || ""}\nPorts: ${d.ports?.map((p) => `${p.host}:${p.container}`).join(", ") || "none"}`}
|
||||||
className={`relative rounded-xl border border-slate-700/80 ${s.bg} backdrop-blur-sm
|
className={`relative rounded-xl border ${s.border} ${s.bg} backdrop-blur-sm
|
||||||
shadow-lg shadow-black/30 p-4 min-w-[220px] ring-2 ${particleGlow ? "" : s.ring}
|
shadow-lg shadow-black/30 p-4 min-w-[220px] ring-2 ${particleGlow ? "" : s.ring}
|
||||||
transition-all duration-300 ${flashClass}`}
|
transition-all duration-300 ${flashClass}`}
|
||||||
style={particleGlow ? {
|
style={particleGlow ? {
|
||||||
@@ -153,28 +153,28 @@ export const ServiceNode = memo(function ServiceNode({ data }: NodeProps) {
|
|||||||
<Handle key={`rt${i}`} type="target" position={Position.Right} id={`right-${i}-target`} className={hdot(`right-${i}`)} style={{ top: o, transform: "translate(50%, -50%)" }} />
|
<Handle key={`rt${i}`} type="target" position={Position.Right} id={`right-${i}-target`} className={hdot(`right-${i}`)} style={{ top: o, transform: "translate(50%, -50%)" }} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Header: icon + name + status dot */}
|
{/* Top section: icon left, name + image right */}
|
||||||
<div className="flex items-center gap-2.5 mb-2">
|
<div className="flex gap-3 mb-2">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-lg"
|
className="flex items-center justify-center w-9 h-9 rounded-lg shrink-0 self-center"
|
||||||
style={{ backgroundColor: `${iconColor}22` }}
|
style={{ backgroundColor: `${iconColor}22` }}
|
||||||
>
|
>
|
||||||
<Icon size={18} style={{ color: iconColor }} />
|
<Icon size={20} style={{ color: iconColor }} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-white text-sm truncate">{d.label}</span>
|
||||||
|
<div className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 truncate mt-0.5">
|
||||||
|
{d.image.startsWith("sha256:") ? `Sin Tag (${d.image.slice(7, 19)})` : d.image}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-white text-sm truncate">{d.label}</span>
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 rounded-full shrink-0 ${s.dot}
|
|
||||||
${d.state === "running" ? "animate-pulse" : ""}`}
|
|
||||||
/>
|
|
||||||
<div className="flex-1" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image */}
|
|
||||||
<div className="text-xs text-slate-500 truncate mb-2 pl-10">{d.image}</div>
|
|
||||||
|
|
||||||
{/* Ports */}
|
{/* Ports */}
|
||||||
{d.ports?.length > 0 && (
|
{d.ports?.length > 0 && (
|
||||||
<div className="flex gap-1.5 flex-wrap mb-2 pl-10">
|
<div className="flex gap-1.5 flex-wrap mb-2">
|
||||||
{d.ports.map((p) => (
|
{d.ports.map((p) => (
|
||||||
<span
|
<span
|
||||||
key={`${p.host}:${p.container}`}
|
key={`${p.host}:${p.container}`}
|
||||||
|
|||||||
+67
-13
@@ -139,21 +139,12 @@ function cleanupLogStream(ws: WebSocket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Docker events ──
|
// ── Docker events ──
|
||||||
watchDockerEvents((event) => {
|
async function refreshServices() {
|
||||||
broadcast({ type: "docker_event", data: event });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Stats polling ──
|
|
||||||
let lastServicesHash = "";
|
|
||||||
let lastConnectionsHash = "";
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
try {
|
||||||
const services = await discoverServices(ALL, PROJECTS);
|
const services = await discoverServices(ALL, PROJECTS);
|
||||||
const connections = await discoverConnections(services);
|
const connections = await discoverConnections(services);
|
||||||
const stats = await pollStats(services);
|
const stats = await pollStats(services);
|
||||||
|
|
||||||
// Only send services/connections if changed
|
|
||||||
const svcHash = services.map((s) => `${s.uid}:${s.state}`).join("|");
|
const svcHash = services.map((s) => `${s.uid}:${s.state}`).join("|");
|
||||||
if (svcHash !== lastServicesHash) {
|
if (svcHash !== lastServicesHash) {
|
||||||
lastServicesHash = svcHash;
|
lastServicesHash = svcHash;
|
||||||
@@ -166,12 +157,55 @@ setInterval(async () => {
|
|||||||
broadcast({ type: "connections", data: connections });
|
broadcast({ type: "connections", data: connections });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats always change (cpu/mem fluctuate)
|
|
||||||
broadcast({ type: "stats", data: stats });
|
broadcast({ type: "stats", data: stats });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Poll error:", err);
|
console.error("Refresh error:", err);
|
||||||
}
|
}
|
||||||
}, POLL_INTERVAL_MS);
|
}
|
||||||
|
|
||||||
|
// Quick refresh — services + connections, no stats (fast)
|
||||||
|
async function quickRefresh() {
|
||||||
|
try {
|
||||||
|
const services = await discoverServices(ALL, PROJECTS);
|
||||||
|
const svcHash = services.map((s) => `${s.uid}:${s.state}`).join("|");
|
||||||
|
if (svcHash !== lastServicesHash) {
|
||||||
|
lastServicesHash = svcHash;
|
||||||
|
broadcast({ type: "services", data: services });
|
||||||
|
|
||||||
|
// Also refresh connections when services change
|
||||||
|
const connections = await discoverConnections(services);
|
||||||
|
const connHash = connections.map((c) => `${c.from}:${c.to}`).join("|");
|
||||||
|
if (connHash !== lastConnectionsHash) {
|
||||||
|
lastConnectionsHash = connHash;
|
||||||
|
broadcast({ type: "connections", data: connections });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced refresh for Docker events
|
||||||
|
let refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
function scheduleRefresh() {
|
||||||
|
clearTimeout(refreshTimer);
|
||||||
|
clearTimeout(retryTimer);
|
||||||
|
// First check at 1.5s, retry at 3.5s to catch stragglers (e.g. slow destroy)
|
||||||
|
refreshTimer = setTimeout(() => {
|
||||||
|
quickRefresh();
|
||||||
|
retryTimer = setTimeout(quickRefresh, 2000);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDockerEvents((event) => {
|
||||||
|
broadcast({ type: "docker_event", data: event });
|
||||||
|
scheduleRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stats polling ──
|
||||||
|
let lastServicesHash = "";
|
||||||
|
let lastConnectionsHash = "";
|
||||||
|
|
||||||
|
setInterval(refreshServices, POLL_INTERVAL_MS);
|
||||||
|
|
||||||
// ── Start ──
|
// ── Start ──
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
@@ -199,6 +233,16 @@ const server = Bun.serve({
|
|||||||
if (flowsData.flows.length > 0) {
|
if (flowsData.flows.length > 0) {
|
||||||
try { native.send(JSON.stringify({ type: "flows", data: flowsData })); } catch {}
|
try { native.send(JSON.stringify({ type: "flows", data: flowsData })); } catch {}
|
||||||
}
|
}
|
||||||
|
// Send current services/connections/stats
|
||||||
|
discoverServices(ALL, PROJECTS).then(async (services) => {
|
||||||
|
const connections = await discoverConnections(services);
|
||||||
|
const stats = await pollStats(services);
|
||||||
|
try {
|
||||||
|
native.send(JSON.stringify({ type: "services", data: services }));
|
||||||
|
native.send(JSON.stringify({ type: "connections", data: connections }));
|
||||||
|
native.send(JSON.stringify({ type: "stats", data: stats }));
|
||||||
|
} catch {}
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close(ws) {
|
close(ws) {
|
||||||
@@ -222,6 +266,16 @@ const server = Bun.serve({
|
|||||||
if (flowsData.flows.length > 0) {
|
if (flowsData.flows.length > 0) {
|
||||||
native.send(JSON.stringify({ type: "flows", data: flowsData }));
|
native.send(JSON.stringify({ type: "flows", data: flowsData }));
|
||||||
}
|
}
|
||||||
|
// Send current services/connections/stats immediately
|
||||||
|
discoverServices(ALL, PROJECTS).then(async (services) => {
|
||||||
|
const connections = await discoverConnections(services);
|
||||||
|
const stats = await pollStats(services);
|
||||||
|
try {
|
||||||
|
native.send(JSON.stringify({ type: "services", data: services }));
|
||||||
|
native.send(JSON.stringify({ type: "connections", data: connections }));
|
||||||
|
native.send(JSON.stringify({ type: "stats", data: stats }));
|
||||||
|
} catch {}
|
||||||
|
}).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
native.send(JSON.stringify({ type: "auth_error" }));
|
native.send(JSON.stringify({ type: "auth_error" }));
|
||||||
native.close();
|
native.close();
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function watchDockerEvents(onEvent: (event: DockerEvent) => void) {
|
|||||||
if (event.Type !== "container") continue;
|
if (event.Type !== "container") continue;
|
||||||
|
|
||||||
const action = event.Action?.split(":")[0]; // "health_status: healthy" → "health_status"
|
const action = event.Action?.split(":")[0]; // "health_status: healthy" → "health_status"
|
||||||
if (!["start", "stop", "die", "restart", "health_status"].includes(action)) continue;
|
if (!["start", "stop", "die", "restart", "destroy", "create", "health_status"].includes(action)) continue;
|
||||||
|
|
||||||
const svcName =
|
const svcName =
|
||||||
event.Actor?.Attributes?.["com.docker.compose.service"] ||
|
event.Actor?.Attributes?.["com.docker.compose.service"] ||
|
||||||
|
|||||||
Reference in New Issue
Block a user