Index: lan-frontend/src/components/Visualisations.jsx
===================================================================
--- lan-frontend/src/components/Visualisations.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/components/Visualisations.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -0,0 +1,290 @@
+// // src/pages/Visualizations.jsx
+// import { useState } from "react";
+// import SankeyPanel from "../components/SankeyPanel";
+//
+// export default function Visualizations() {
+//   const [env, setEnv] = useState("office1");
+//
+//   // ✅ All = празни низи
+//   const [computers, setComputers] = useState([]);
+//   const [ips, setIps] = useState([]);
+//
+//   return (
+//     <div>
+//       <h2>Network Sankey Visualization</h2>
+//
+//       <SankeyPanel
+//         env={env}
+//         computers={computers}
+//         ips={ips}
+//         from="now-2d"
+//         to="now"
+//       />
+//     </div>
+//   );
+// }
+
+
+import React, { useEffect, useMemo, useState } from "react";
+import SankeyViz from "../components/SankeyViz";
+
+function MultiSelect({ options, value, onChange, label }) {
+  return (
+    <div style={{ display: "grid", gap: 6 }}>
+      <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>{label}</div>
+      <select
+        multiple
+        value={value}
+        onChange={(e) => {
+          const vals = Array.from(e.target.selectedOptions).map((o) => o.value);
+          onChange(vals);
+        }}
+        className="select"
+        style={{ minHeight: 90 }}
+      >
+        {options.map((o) => (
+          <option key={o} value={o}>
+            {o}
+          </option>
+        ))}
+      </select>
+      <div style={{ fontSize: 11, color: "rgba(255,255,255,0.5)" }}>
+        (држи Ctrl за multi-select)
+      </div>
+    </div>
+  );
+}
+
+export default function Visualizations() {
+  const [env, setEnv] = useState("office1");
+
+  // 👇 ако сакаш env листа динамички, додај endpoint.
+  const envOptions = ["default", "office1"];
+
+  const [computerOptions, setComputerOptions] = useState(["All"]);
+  const [ipOptions, setIpOptions] = useState(["All"]);
+
+  const [computers, setComputers] = useState(["All"]);
+  const [ips, setIps] = useState(["All"]);
+
+  const [hours, setHours] = useState(48);
+  const [limit, setLimit] = useState(200);
+
+  const [vizType, setVizType] = useState("sankey"); // sankey | table | topips
+  const [rows, setRows] = useState([]);
+  const [loading, setLoading] = useState(false);
+
+  // ✅ 1) load computers for env (користи твојот постоечки /api/computers)
+  async function loadComputers() {
+    const r = await fetch("/api/computers", {
+      credentials: "include",
+      headers: { "X-Env": env },
+    });
+    const data = await r.json();
+    const names = (Array.isArray(data) ? data : []).map((c) => c.name).filter(Boolean);
+    setComputerOptions(["All", ...names]);
+  }
+
+  // ✅ 2) load ips list (ОВА треба да го имаш на backend како /api/ips)
+  async function loadIps() {
+    const r = await fetch(`/api/ips?env=${encodeURIComponent(env)}&hours=${hours}&limit=${limit}`, {
+      credentials: "include",
+    });
+    const data = await r.json();
+    const list = Array.isArray(data?.ips) ? data.ips : Array.isArray(data) ? data : [];
+    setIpOptions(["All", ...list]);
+  }
+
+  // ✅ 3) load sankey rows (ОВА треба да го имаш на backend како /api/sankey)
+  async function loadSankey() {
+    setLoading(true);
+    try {
+      const params = new URLSearchParams();
+      params.set("env", env);
+      params.set("hours", String(hours));
+      params.set("limit", String(limit));
+      params.set("computers", (computers?.length ? computers : ["All"]).join(","));
+      params.set("ips", (ips?.length ? ips : ["All"]).join(","));
+
+      const r = await fetch(`/api/sankey?${params.toString()}`, { credentials: "include" });
+      const data = await r.json();
+      setRows(Array.isArray(data) ? data : []);
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    loadComputers();
+    loadIps();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [env, hours, limit]);
+
+  useEffect(() => {
+    loadSankey();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [env, hours, limit, computers, ips]);
+
+  // Table view
+  const tableRows = useMemo(() => rows.slice(0, 200), [rows]);
+
+  // Top IPs view
+  const topIps = useMemo(() => {
+    const map = new Map();
+    for (const r of rows) {
+      map.set(r.target, (map.get(r.target) || 0) + Number(r.value || 0));
+    }
+    return Array.from(map.entries())
+      .sort((a, b) => b[1] - a[1])
+      .slice(0, 20);
+  }, [rows]);
+
+  return (
+    <div className="panel" style={{ marginTop: 14 }}>
+      <div className="panel-head">
+        <div className="panel-title">Visualizations</div>
+        <div style={{ color: "var(--dim)", fontSize: 12 }}>
+          {loading ? "Loading…" : `${rows.length} links`}
+        </div>
+      </div>
+
+      <div className="panel-body" style={{ display: "grid", gap: 14 }}>
+        {/* Controls */}
+        <div
+          style={{
+            display: "grid",
+            gridTemplateColumns: "1fr 1fr 1fr",
+            gap: 12,
+            alignItems: "start",
+          }}
+        >
+          <div style={{ display: "grid", gap: 8 }}>
+            <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Environment</div>
+            <select className="select" value={env} onChange={(e) => setEnv(e.target.value)}>
+              {envOptions.map((x) => (
+                <option key={x} value={x}>
+                  {x}
+                </option>
+              ))}
+            </select>
+
+            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
+              <div>
+                <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Hours</div>
+                <input
+                  className="input"
+                  type="number"
+                  value={hours}
+                  min={1}
+                  max={720}
+                  onChange={(e) => setHours(Number(e.target.value))}
+                />
+              </div>
+              <div>
+                <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Limit</div>
+                <input
+                  className="input"
+                  type="number"
+                  value={limit}
+                  min={10}
+                  max={2000}
+                  onChange={(e) => setLimit(Number(e.target.value))}
+                />
+              </div>
+            </div>
+
+            <div style={{ display: "grid", gap: 6 }}>
+              <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Visualization</div>
+              <select className="select" value={vizType} onChange={(e) => setVizType(e.target.value)}>
+                <option value="sankey">Sankey</option>
+                <option value="table">Table</option>
+                <option value="topips">Top IPs</option>
+              </select>
+            </div>
+
+            <button
+              className="btn"
+              onClick={() => {
+                setComputers(["All"]);
+                setIps(["All"]);
+              }}
+            >
+              Reset filters
+            </button>
+          </div>
+
+          <MultiSelect
+            label="Computers"
+            options={computerOptions}
+            value={computers}
+            onChange={(vals) => setComputers(vals.length ? vals : ["All"])}
+          />
+
+          <MultiSelect
+            label="IPs"
+            options={ipOptions}
+            value={ips}
+            onChange={(vals) => setIps(vals.length ? vals : ["All"])}
+          />
+        </div>
+
+        {/* Viz */}
+        {vizType === "sankey" && <SankeyViz dataRows={rows} height={520} />}
+
+        {vizType === "table" && (
+          <div style={{ overflow: "auto", border: "1px solid rgba(255,255,255,0.08)", borderRadius: 12 }}>
+            <table style={{ width: "100%", borderCollapse: "collapse" }}>
+              <thead>
+                <tr>
+                  <th style={{ textAlign: "left", padding: 10, color: "rgba(255,255,255,0.8)" }}>Source</th>
+                  <th style={{ textAlign: "left", padding: 10, color: "rgba(255,255,255,0.8)" }}>Target</th>
+                  <th style={{ textAlign: "right", padding: 10, color: "rgba(255,255,255,0.8)" }}>Value</th>
+                </tr>
+              </thead>
+              <tbody>
+                {tableRows.map((r, idx) => (
+                  <tr key={idx} style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
+                    <td style={{ padding: 10, color: "rgba(255,255,255,0.85)" }}>{r.source}</td>
+                    <td style={{ padding: 10, color: "rgba(255,255,255,0.85)" }}>{r.target}</td>
+                    <td style={{ padding: 10, textAlign: "right", color: "rgba(255,255,255,0.85)" }}>{r.value}</td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          </div>
+        )}
+
+        {vizType === "topips" && (
+          <div style={{ display: "grid", gap: 8 }}>
+            {topIps.map(([ip, v]) => (
+              <div
+                key={ip}
+                style={{
+                  display: "grid",
+                  gridTemplateColumns: "220px 1fr 80px",
+                  gap: 10,
+                  alignItems: "center",
+                  padding: "8px 10px",
+                  border: "1px solid rgba(255,255,255,0.08)",
+                  borderRadius: 12,
+                }}
+              >
+                <div style={{ color: "rgba(255,255,255,0.85)" }}>{ip}</div>
+                <div style={{ height: 10, background: "rgba(255,255,255,0.08)", borderRadius: 99, overflow: "hidden" }}>
+                  <div
+                    style={{
+                      height: "100%",
+                      width: `${Math.min(100, (v / (topIps[0]?.[1] || 1)) * 100)}%`,
+                      background: "rgba(80,160,255,0.7)",
+                    }}
+                  />
+                </div>
+                <div style={{ textAlign: "right", color: "rgba(255,255,255,0.8)" }}>{v}</div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
