Index: lan-frontend/.env
===================================================================
--- lan-frontend/.env	(revision d42aac31fe37e6e42a1b0515b54b86e8706d90a8)
+++ lan-frontend/.env	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -1,2 +1,3 @@
 VITE_API_BASE=http://localhost:5555
 VITE_GOOGLE_CLIENT_ID=43542590862-pt18e0hb33o41889bu976oshddoemivs.apps.googleusercontent.com
+GRAFANA_API_TOKEN=KIC7rMwr4U8V-GI8suP5rGgNjdcQvB0cAvAwkwevTKw
Index: lan-frontend/src/components/AIAssistantDrawer.jsx
===================================================================
--- lan-frontend/src/components/AIAssistantDrawer.jsx	(revision d42aac31fe37e6e42a1b0515b54b86e8706d90a8)
+++ lan-frontend/src/components/AIAssistantDrawer.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -73,9 +73,15 @@
                 ]);
             } else {
-                setMessages((m) => [
-                    ...m,
-                    {role: "assistant", text: data.answer || "(no answer)"},
-                ]);
+                const qs = Array.isArray(data.queries) ? data.queries : [];
+                const notes = data.notes || "";
+
+                const text =
+                    qs.length
+                        ? `SQL queries:\n\n${qs.map((q, i) => `${i + 1}) ${q}`).join("\n\n")}\n\nNotes: ${notes}`
+                        : `Notes: ${notes || "No queries returned."}`;
+
+                setMessages((m) => [...m, {role: "assistant", text}]);
             }
+
 
         } catch (e) {
Index: lan-frontend/src/components/Dropdown.jsx
===================================================================
--- lan-frontend/src/components/Dropdown.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/components/Dropdown.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import SankeyPanel from "../components/SankeyPanel";
+
+export default function VisualizationsPage() {
+  const [env, setEnv] = useState("office1");
+  const [computers, setComputers] = useState(["All"]);
+  const [ips, setIps] = useState(["All"]);
+
+  return (
+    <div style={{ padding: 16 }}>
+      <h2>Network Sankey</h2>
+
+      {/* Пример UI (ти можеш да ставиш multi-select компонента) */}
+      <div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
+        <select value={env} onChange={(e) => setEnv(e.target.value)}>
+          <option value="office1">office1</option>
+          <option value="default">default</option>
+        </select>
+
+        <select
+          value={computers[0]}
+          onChange={(e) => setComputers([e.target.value])}
+        >
+          <option value="All">All</option>
+          <option value="Ilina-laptop">Ilina-laptop</option>
+          <option value="Irena">Irena</option>
+        </select>
+
+        <select value={ips[0]} onChange={(e) => setIps([e.target.value])}>
+          <option value="All">All</option>
+          <option value="127.0.0.1:443">127.0.0.1:443</option>
+        </select>
+      </div>
+
+      <SankeyPanel
+        dashboardUid="adt9v8k"   // <- стави го твојот UID од Grafana URL
+        env={env}
+        computers={computers}
+        ips={ips}
+        from="now-24h"
+        to="now"
+      />
+    </div>
+  );
+}
Index: lan-frontend/src/components/SankeyPanel.jsx
===================================================================
--- lan-frontend/src/components/SankeyPanel.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/components/SankeyPanel.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -0,0 +1,45 @@
+// src/components/SankeyPanel.jsx
+import { useMemo } from "react";
+
+export default function SankeyPanel({
+  env = "office1",
+  computers = [],   // ✅ празно значи All
+  ips = [],         // ✅ празно значи All
+  from = "now-24h",
+  to = "now",
+}) {
+  const dashboardUid = "adt9v8k";
+  const grafanaBase = "http://localhost:3000";
+
+  const src = useMemo(() => {
+    const params = new URLSearchParams();
+    params.set("orgId", "1");
+    params.set("kiosk", "tv");
+    params.set("from", from);
+    params.set("to", to);
+
+    params.set("var-env", env);
+
+    // ✅ НЕ праќај "All"
+    const cleanComputers = (computers || []).filter((c) => c && c !== "All");
+    const cleanIps = (ips || []).filter((ip) => ip && ip !== "All");
+
+    // ✅ додавај само ако има конкретни вредности
+    cleanComputers.forEach((c) => params.append("var-computers", c));
+    cleanIps.forEach((ip) => params.append("var-ips", ip));
+
+    return `${grafanaBase}/d/${dashboardUid}/sankey-panel?${params.toString()}`;
+  }, [env, computers, ips, from, to]);
+
+  return (
+    <div style={{ width: "100%", height: "85vh", overflow: "hidden", borderRadius: 12 }}>
+      <iframe
+        src={src}
+        title="Network Sankey"
+        width="100%"
+        height="100%"
+        frameBorder="0"
+      />
+    </div>
+  );
+}
Index: lan-frontend/src/components/SankeyViz.jsx
===================================================================
--- lan-frontend/src/components/SankeyViz.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
+++ lan-frontend/src/components/SankeyViz.jsx	(revision e4c61dd6cd86e06265bc2bd91adba84a0f04044a)
@@ -0,0 +1,119 @@
+import React, { useEffect, useMemo, useRef } from "react";
+import * as d3 from "d3";
+import { sankey, sankeyLinkHorizontal } from "d3-sankey";
+
+/**
+ * dataRows: [{ source: "PC1", target: "1.2.3.4:443", value: 12 }, ...]
+ */
+export default function SankeyViz({ dataRows, height = 520 }) {
+  const ref = useRef(null);
+
+  const graph = useMemo(() => {
+    const rows = Array.isArray(dataRows) ? dataRows : [];
+    const nodesMap = new Map();
+    const nodes = [];
+    const links = [];
+
+    function getNode(name) {
+      if (!nodesMap.has(name)) {
+        const id = nodes.length;
+        nodesMap.set(name, id);
+        nodes.push({ name });
+      }
+      return nodesMap.get(name);
+    }
+
+    for (const r of rows) {
+      if (!r?.source || !r?.target) continue;
+      const s = getNode(String(r.source));
+      const t = getNode(String(r.target));
+      const v = Number(r.value ?? 1);
+      links.push({ source: s, target: t, value: isFinite(v) ? v : 1 });
+    }
+
+    return { nodes, links };
+  }, [dataRows]);
+
+  useEffect(() => {
+    const el = ref.current;
+    if (!el) return;
+
+    el.innerHTML = "";
+
+    const width = el.clientWidth || 900;
+    const svg = d3
+      .select(el)
+      .append("svg")
+      .attr("width", "100%")
+      .attr("viewBox", `0 0 ${width} ${height}`)
+      .style("background", "rgba(255,255,255,0.02)")
+      .style("border", "1px solid rgba(255,255,255,0.08)")
+      .style("borderRadius", "14px");
+
+    if (!graph.nodes.length || !graph.links.length) {
+      svg
+        .append("text")
+        .attr("x", width / 2)
+        .attr("y", height / 2)
+        .attr("text-anchor", "middle")
+        .style("fill", "rgba(255,255,255,0.7)")
+        .style("font-size", "14px")
+        .text("No data for this selection.");
+      return;
+    }
+
+    const sk = sankey()
+      .nodeWidth(18)
+      .nodePadding(14)
+      .extent([
+        [20, 20],
+        [width - 20, height - 20],
+      ]);
+
+    const { nodes, links } = sk({
+      nodes: graph.nodes.map((d) => ({ ...d })),
+      links: graph.links.map((d) => ({ ...d })),
+    });
+
+    // Links
+    svg
+      .append("g")
+      .attr("fill", "none")
+      .selectAll("path")
+      .data(links)
+      .join("path")
+      .attr("d", sankeyLinkHorizontal())
+      .attr("stroke", "rgba(80,160,255,0.55)")
+      .attr("stroke-width", (d) => Math.max(1, d.width))
+      .attr("opacity", 0.9);
+
+    // Nodes
+    const node = svg
+      .append("g")
+      .selectAll("g")
+      .data(nodes)
+      .join("g");
+
+    node
+      .append("rect")
+      .attr("x", (d) => d.x0)
+      .attr("y", (d) => d.y0)
+      .attr("height", (d) => Math.max(1, d.y1 - d.y0))
+      .attr("width", (d) => d.x1 - d.x0)
+      .attr("rx", 6)
+      .attr("fill", "rgba(255,255,255,0.18)")
+      .attr("stroke", "rgba(255,255,255,0.15)");
+
+    node
+      .append("text")
+      .attr("x", (d) => (d.x0 < width / 2 ? d.x1 + 8 : d.x0 - 8))
+      .attr("y", (d) => (d.y0 + d.y1) / 2)
+      .attr("dy", "0.35em")
+      .attr("text-anchor", (d) => (d.x0 < width / 2 ? "start" : "end"))
+      .style("fill", "rgba(255,255,255,0.85)")
+      .style("font-size", "12px")
+      .text((d) => d.name);
+  }, [graph, height]);
+
+  return <div ref={ref} style={{ width: "100%" }} />;
+}
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>
+  );
+}
