File transfer

This commit is contained in:
2026-06-11 20:33:44 -05:00
parent 2cb1d58264
commit b362357bcb
58 changed files with 5096 additions and 4 deletions

View File

@@ -0,0 +1,103 @@
/**
* Flow Graph — container-to-container traffic visualization
* Uses a simple SVG force-directed layout (D3-free for bundle size)
* Ball Studios <https://git.balls.studio>
*/
import React, { useEffect, useRef } from 'react';
interface Flow {
src_server_id: number | null;
dst_server_id: number | null;
bytes: number;
packets: number;
}
interface Props { flows: Flow[]; }
export default function FlowGraph({ flows }: Props) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current || flows.length === 0) return;
const svg = svgRef.current;
const W = svg.clientWidth || 600;
const H = 280;
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
// Collect unique server IDs + "Internet" (null src)
const serverIds = new Set<string>();
flows.forEach(f => {
serverIds.add(f.src_server_id ? `s${f.src_server_id}` : 'internet');
serverIds.add(f.dst_server_id ? `s${f.dst_server_id}` : 'internet');
});
const nodes = Array.from(serverIds);
// Layout: evenly spaced horizontally
const positions: Record<string, { x: number; y: number }> = {};
nodes.forEach((id, i) => {
positions[id] = { x: (W / (nodes.length + 1)) * (i + 1), y: H / 2 };
});
const maxBytes = Math.max(...flows.map(f => f.bytes), 1);
// Clear
while (svg.firstChild) svg.removeChild(svg.firstChild);
// Draw edges
flows.forEach(f => {
const srcId = f.src_server_id ? `s${f.src_server_id}` : 'internet';
const dstId = f.dst_server_id ? `s${f.dst_server_id}` : 'internet';
if (!positions[srcId] || !positions[dstId]) return;
const src = positions[srcId];
const dst = positions[dstId];
const strokeW = 1 + (f.bytes / maxBytes) * 5;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', String(src.x)); line.setAttribute('y1', String(src.y));
line.setAttribute('x2', String(dst.x)); line.setAttribute('y2', String(dst.y));
line.setAttribute('stroke', '#6366f1');
line.setAttribute('stroke-width', String(strokeW));
line.setAttribute('stroke-opacity', '0.6');
svg.appendChild(line);
// Arrow marker
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const midX = (src.x + dst.x) / 2;
const midY = (src.y + dst.y) / 2;
arrow.setAttribute('points', `${midX},${midY - 4} ${midX + 8},${midY} ${midX},${midY + 4}`);
arrow.setAttribute('fill', '#818cf8');
svg.appendChild(arrow);
});
// Draw nodes
nodes.forEach(id => {
const pos = positions[id];
const isNet = id === 'internet';
const label = isNet ? '🌐 Internet' : `Server ${id.slice(1)}`;
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', String(pos.x)); circle.setAttribute('cy', String(pos.y));
circle.setAttribute('r', '28');
circle.setAttribute('fill', isNet ? '#1e3a5f' : '#1e1b4b');
circle.setAttribute('stroke', isNet ? '#60a5fa' : '#818cf8');
circle.setAttribute('stroke-width', '2');
svg.appendChild(circle);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', String(pos.x)); text.setAttribute('y', String(pos.y + 5));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('fill', '#e5e7eb');
text.setAttribute('font-size', '11');
text.textContent = label;
svg.appendChild(text);
});
}, [flows]);
if (flows.length === 0) {
return <div className='addon-empty'>No inter-container flows detected in the last 24h.</div>;
}
return <svg ref={svgRef} className='addon-flow-graph' width='100%' height='280' />;
}