Changeset 505f39a for lan-frontend


Ignore:
Timestamp:
01/21/26 13:35:44 (6 months ago)
Author:
istevanoska <ilinastevanoska@…>
Branches:
master
Children:
d42aac3
Parents:
2058e5c
Message:

Added OAuth/prototype

Location:
lan-frontend
Files:
3 added
11 edited

Legend:

Unmodified
Added
Removed
  • lan-frontend/.env

    r2058e5c r505f39a  
    1 VITE_API_BASE=http://192.168.11.232:5555
     1VITE_API_BASE=http://localhost:5555
     2VITE_GOOGLE_CLIENT_ID=43542590862-pt18e0hb33o41889bu976oshddoemivs.apps.googleusercontent.com
  • lan-frontend/package-lock.json

    r2058e5c r505f39a  
    99      "version": "0.0.0",
    1010      "dependencies": {
     11        "@react-oauth/google": "^0.13.4",
    1112        "chart.js": "^4.5.1",
    1213        "react": "^19.2.0",
     
    10141015      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
    10151016      "license": "MIT"
     1017    },
     1018    "node_modules/@react-oauth/google": {
     1019      "version": "0.13.4",
     1020      "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz",
     1021      "integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==",
     1022      "license": "MIT",
     1023      "peerDependencies": {
     1024        "react": ">=16.8.0",
     1025        "react-dom": ">=16.8.0"
     1026      }
    10161027    },
    10171028    "node_modules/@reduxjs/toolkit": {
  • lan-frontend/package.json

    r2058e5c r505f39a  
    1111  },
    1212  "dependencies": {
     13    "@react-oauth/google": "^0.13.4",
    1314    "chart.js": "^4.5.1",
    1415    "react": "^19.2.0",
  • lan-frontend/src/App.jsx

    r2058e5c r505f39a  
    1 import React from "react";
    2 import {useEffect, useMemo, useState} from "react";
     1import React, {useEffect, useMemo, useState} from "react";
    32import "./styles/theme.css";
    43
     4import Login from "./components/Login";
    55import TopBar from "./components/TopBar";
    66import ComputerCard from "./components/ComputerCard";
     
    2222    const [selectedEnv, setSelectedEnv] = useState("default");
    2323
     24    const [me, setMe] = useState(null);
     25    const [meLoading, setMeLoading] = useState(true);
     26
     27    // const [openAI, setOpenAI] = useState(false);
     28    // const [scope, setScope] = useState(null);
     29
     30
     31    async function loadMe() {
     32        setMeLoading(true);
     33        try {
     34            const r = await fetch("/api/me", {credentials: "include"});
     35            if (r.ok) {
     36                const data = await r.json();
     37                setMe(data.user);
     38            } else {
     39                setMe(null);
     40            }
     41        } catch (e) {
     42            console.error("loadMe error", e);
     43            setMe(null);
     44        } finally {
     45            setMeLoading(false);
     46        }
     47    }
     48
    2449    async function refresh() {
    2550        try {
    2651            setLoading(true);
    2752            const [s, c] = await Promise.all([
    28                 fetch("/api/stats").then((r) => r.json()),
     53                fetch("/api/stats", {credentials: "include"}).then((r) => r.json()),
    2954                fetch("/api/computers", {
     55                    credentials: "include",
    3056                    headers: {"X-Env": selectedEnv},
    3157                }).then((r) => r.json()),
    3258            ]);
     59
    3360            setStats(s);
    3461            setComputers(Array.isArray(c) ? c : []);
     
    4168    }
    4269
     70    // on mount: check session
    4371    useEffect(() => {
    44         refresh();
    45     }, [selectedEnv]);
     72        loadMe();
     73    }, []);
     74
     75    // when logged in OR env changed, refresh data
     76    useEffect(() => {
     77        if (me) refresh();
     78        // eslint-disable-next-line react-hooks/exhaustive-deps
     79    }, [me, selectedEnv]);
    4680
    4781    const filtered = useMemo(() => {
     
    5185            .filter((c) => {
    5286                if (!q) return true;
    53                 const hay = [c.name, c.user, c.ip, c.os, c.status].filter(Boolean).join(" ").toLowerCase();
     87                const hay = [c.name, c.user, c.ip, c.os, c.status]
     88                    .filter(Boolean)
     89                    .join(" ")
     90                    .toLowerCase();
    5491                return hay.includes(q);
    5592            });
    5693    }, [computers, query, status]);
     94
     95    // Gate UI
     96    if (meLoading) return <div style={{padding: 20}}>Loading…</div>;
     97    if (!me) return <Login onDone={loadMe}/>;
    5798
    5899    return (
     
    64105                onEnvChange={(env) => setSelectedEnv(env)}
    65106                selectedEnv={selectedEnv}
     107                me={me}
     108                onLoggedOut={loadMe}
    66109            />
     110
    67111
    68112            {/* STATS */}
     
    104148                        />
    105149
    106                         <select className="select" value={status} onChange={(e) => setStatus(e.target.value)}>
     150                        <select
     151                            className="select"
     152                            value={status}
     153                            onChange={(e) => setStatus(e.target.value)}
     154                        >
    107155                            <option value="">All status</option>
    108156                            <option value="online">Online</option>
     
    111159                        </select>
    112160
    113                         <button className="btn primary" onClick={refresh}>Refresh</button>
     161                        <button className="btn primary" onClick={refresh}>
     162                            Refresh
     163                        </button>
    114164                    </div>
    115165                </div>
     
    122172                    <div style={{display: "flex", gap: 10, alignItems: "center"}}>
    123173                        <span className="pill">{filtered.length} computers</span>
    124                         <button className="btn" onClick={() => {
    125                             setQuery("");
    126                             setStatus("");
    127                         }}>Reset
     174                        <button
     175                            className="btn"
     176                            onClick={() => {
     177                                setQuery("");
     178                                setStatus("");
     179                            }}
     180                        >
     181                            Reset
    128182                        </button>
    129183                    </div>
     
    142196                        ))}
    143197                        {filtered.length === 0 && (
    144                             <div style={{color: "var(--dim)"}}>No computers match filter.</div>
     198                            <div style={{color: "var(--dim)"}}>
     199                                No computers match filter.
     200                            </div>
    145201                        )}
    146202                    </div>
     
    160216
    161217            {/* AI DRAWER */}
     218            {/*<button onClick={() => setOpenAI(true)}>AI</button>*/}
     219
     220            {/*<AIAssistantDrawer*/}
     221            {/*    open={openAI}*/}
     222            {/*    onClose={() => setOpenAI(false)}*/}
     223            {/*    computers={computers}*/}
     224            {/*    scope={scope}*/}
     225            {/*    setScope={setScope}*/}
     226            {/*    apiBase={import.meta.env.VITE_API_BASE}*/}
     227            {/*/>*/}
     228
    162229            <AIAssistantDrawer
    163230                open={aiOpen}
     
    166233                scope={aiScope}
    167234                setScope={setAiScope}
     235                apiBase={import.meta.env.VITE_API_BASE}
    168236            />
     237
     238
    169239        </div>
    170240    );
  • lan-frontend/src/components/AIAssistantDrawer.jsx

    r2058e5c r505f39a  
    1 import React from "react";
     1import React, {useEffect, useRef, useState} from "react";
    22
    3 import { useEffect, useRef, useState } from "react";
     3export default function AIAssistantDrawer({
     4                                              open,
     5                                              onClose,
     6                                              computers,
     7                                              scope,
     8                                              setScope,
     9                                              apiBase = "", // ако сакаш можеш да пратиш VITE_API_BASE тука
     10                                          }) {
     11    const [messages, setMessages] = useState([
     12        {
     13            role: "assistant",
     14            text: "Постави прашање за Sysmon, процеси, мрежа. Можеш да избереш компјутер (scope) горе.",
     15        },
     16    ]);
     17    const [input, setInput] = useState("");
     18    const [sending, setSending] = useState(false);
     19    const bottomRef = useRef(null);
    420
    5 export default function AIAssistantDrawer({ open, onClose, computers, scope, setScope }) {
    6   const [messages, setMessages] = useState([
    7     { role: "assistant", text: "Постави прашање за Sysmon, процеси, мрежа. Можеш да избереш компјутер (scope) горе." },
    8   ]);
    9   const [input, setInput] = useState("");
    10   const [sending, setSending] = useState(false);
    11   const bottomRef = useRef(null);
     21    useEffect(() => {
     22        if (open) {
     23            // затвори со Escape
     24            const onKey = (e) => {
     25                if (e.key === "Escape") onClose?.();
     26            };
     27            window.addEventListener("keydown", onKey);
     28            return () => window.removeEventListener("keydown", onKey);
     29        }
     30    }, [open, onClose]);
    1231
    13   useEffect(() => {
    14     if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: "smooth" });
    15   }, [messages, open]);
     32    useEffect(() => {
     33        if (bottomRef.current) bottomRef.current.scrollIntoView({behavior: "smooth"});
     34    }, [messages, open]);
    1635
    17   async function send() {
    18     const q = input.trim();
    19     if (!q || sending) return;
     36    async function send() {
     37        const q = input.trim();
     38        if (!q || sending) return;
    2039
    21     setInput("");
    22     setMessages((m) => [...m, { role: "user", text: q }]);
    23     setSending(true);
     40        setInput("");
     41        setMessages((m) => [...m, {role: "user", text: q}]);
     42        setSending(true);
    2443
    25     try {
    26       const r = await fetch("/api/chat", {
    27         method: "POST",
    28         headers: { "Content-Type": "application/json" },
    29         body: JSON.stringify({ question: q, computer_name: scope || null }),
    30       });
    31       const data = await r.json();
    32       setMessages((m) => [...m, { role: "assistant", text: data.answer || "(no answer)" }]);
    33     } catch (e) {
    34       setMessages((m) => [...m, { role: "assistant", text: `Грешка: ${String(e.message || e)}` }]);
    35     } finally {
    36       setSending(false);
     44        try {
     45            const base = (apiBase || "").replace(/\/$/, ""); // тргни trailing /
     46            const url = `${base}/api/chat`;
     47
     48            const r = await fetch(url, {
     49                method: "POST",
     50                headers: {"Content-Type": "application/json"},
     51                credentials: "include",
     52                body: JSON.stringify({question: q, computer_name: scope || null}),
     53            });
     54
     55
     56            const data = await r.json().catch(() => ({}));
     57            if (!r.ok) {
     58                setMessages((m) => [...m, {
     59                    role: "assistant",
     60                    text: `Error ${r.status}: ${data.error || "Request failed"}`
     61                }]);
     62            } else {
     63                setMessages((m) => [...m, {role: "assistant", text: data.answer || "(no answer)"}]);
     64            }
     65            setMessages((m) => [
     66                ...m,
     67                {role: "assistant", text: data.answer || "(no answer)"},
     68            ]);
     69        } catch (e) {
     70            setMessages((m) => [
     71                ...m,
     72                {role: "assistant", text: `Грешка: ${String(e?.message || e)}`},
     73            ]);
     74        } finally {
     75            setSending(false);
     76        }
    3777    }
    38   }
    3978
    40   if (!open) return null;
     79    if (!open) return null;
    4180
    42   return (
    43     <>
    44       <div className="drawer-backdrop" onMouseDown={onClose} />
    45       <div className="drawer" onMouseDown={(e) => e.stopPropagation()}>
    46         <div className="drawer-head">
    47           <div>
    48             <div className="drawer-title">🤖 AI Assistant</div>
    49             <div className="drawer-sub">Uses /api/chat (RAG over your logs)</div>
    50           </div>
    51           <button className="btn" onClick={onClose}>Close</button>
    52         </div>
     81    return (
     82        <>
     83            <div className="aiDrawer-backdrop" onMouseDown={onClose}/>
    5384
    54         <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)" }}>
    55           <div className="row">
    56             <select className="select" value={scope || ""} onChange={(e) => setScope(e.target.value || null)}>
    57               <option value="">All computers</option>
    58               {(computers || []).map((c) => (
    59                 <option key={c.name} value={c.name}>{c.name}</option>
    60               ))}
    61             </select>
    62           </div>
    63           <div className="small">Tip: “Top процеси по RAM” или “сомнителни sysmon events”</div>
    64         </div>
     85            <aside
     86                className="aiDrawer"
     87                role="dialog"
     88                aria-modal="true"
     89                onMouseDown={(e) => e.stopPropagation()}
     90            >
     91                <header className="aiDrawer-head">
     92                    <div>
     93                        <div className="aiDrawer-title">🤖 AI Assistant</div>
     94                        <div className="aiDrawer-sub">RAG over your logs</div>
     95                    </div>
    6596
    66         <div className="drawer-body">
    67           {messages.map((m, i) => (
    68             <div key={i} className={`bubble ${m.role === "user" ? "user" : ""}`}>
    69               <div style={{ fontSize: 11, color: "var(--dim)", marginBottom: 6 }}>
    70                 {m.role === "user" ? "You" : "Assistant"}
    71               </div>
    72               <div style={{ whiteSpace: "pre-wrap" }}>{m.text}</div>
    73             </div>
    74           ))}
    75           <div ref={bottomRef} />
    76         </div>
     97                    <button className="aiBtn" onClick={onClose} aria-label="Close">
     98                        ✕
     99                    </button>
     100                </header>
    77101
    78         <div className="drawer-foot">
    79           <div className="row">
    80             <textarea
    81               className="textarea"
     102                <div className="aiDrawer-toolbar">
     103                    <select
     104                        className="aiSelect"
     105                        value={scope || ""}
     106                        onChange={(e) => setScope(e.target.value || null)}
     107                    >
     108                        <option value="">All computers</option>
     109                        {(computers || []).map((c) => (
     110                            <option key={c.name} value={c.name}>
     111                                {c.name}
     112                            </option>
     113                        ))}
     114                    </select>
     115
     116                    <div className="aiHint">
     117                        Tip: “Top процеси по RAM” / “сомнителни sysmon events”
     118                    </div>
     119                </div>
     120
     121                <main className="aiDrawer-body">
     122                    {messages.map((m, i) => (
     123                        <div
     124                            key={i}
     125                            className={`aiMsg ${m.role === "user" ? "user" : "assistant"}`}
     126                        >
     127                            <div className="aiMsg-meta">{m.role === "user" ? "You" : "Assistant"}</div>
     128                            <div className="aiMsg-text">{m.text}</div>
     129                        </div>
     130                    ))}
     131                    <div ref={bottomRef}/>
     132                </main>
     133
     134                <footer className="aiDrawer-foot">
     135          <textarea
     136              className="aiTextarea"
    82137              rows={2}
    83138              value={input}
     
    85140              onChange={(e) => setInput(e.target.value)}
    86141              onKeyDown={(e) => {
    87                 if (e.key === "Enter" && !e.shiftKey) {
    88                   e.preventDefault();
    89                   send();
    90                 }
     142                  if (e.key === "Enter" && !e.shiftKey) {
     143                      e.preventDefault();
     144                      send();
     145                  }
    91146              }}
    92             />
    93             <button className="btn primary" onClick={send} disabled={sending || !input.trim()}>
    94               {sending ? "Sending…" : "Send"}
    95             </button>
    96           </div>
    97         </div>
    98       </div>
    99     </>
    100   );
     147          />
     148                    <button
     149                        className="aiBtn aiBtnPrimary"
     150                        onClick={send}
     151                        disabled={sending || !input.trim()}
     152                    >
     153                        {sending ? "Sending…" : "Send"}
     154                    </button>
     155                </footer>
     156            </aside>
     157        </>
     158    );
    101159}
  • lan-frontend/src/components/ComputerDetailsModal.jsx

    r2058e5c r505f39a  
    22
    33function severityForEventId(eventId) {
    4   const id = parseInt(eventId, 10);
     4  const id = Number.parseInt(String(eventId ?? ""), 10);
    55  const high = new Set([1, 3, 8, 10]);
    66  const med = new Set([5, 6, 7, 11, 22]);
     
    1010}
    1111
     12function fmtTime(ts) {
     13  if (!ts) return "—";
     14  try {
     15    return new Date(ts).toLocaleTimeString();
     16  } catch {
     17    return String(ts);
     18  }
     19}
     20
    1221export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) {
    1322  const [tab, setTab] = useState("overview");
    1423  const [loading, setLoading] = useState(false);
    1524  const [details, setDetails] = useState(null);
     25  const [error, setError] = useState("");
    1626  const [search, setSearch] = useState("");
    1727
    18   // JSON viewer state
     28  // JSON viewer
    1929  const [jsonOpen, setJsonOpen] = useState(false);
    2030  const [jsonTitle, setJsonTitle] = useState("");
    2131  const [jsonData, setJsonData] = useState(null);
    2232
     33  // reset when opening / switching pc
     34  useEffect(() => {
     35    if (!open) return;
     36    setTab("overview");
     37    setSearch("");
     38    setError("");
     39    setDetails(null);
     40    setJsonOpen(false);
     41    setJsonTitle("");
     42    setJsonData(null);
     43  }, [open, computer?.name]);
     44
     45  // close on ESC
     46  useEffect(() => {
     47    if (!open) return;
     48    const onKey = (e) => {
     49      if (e.key === "Escape") onClose?.();
     50    };
     51    window.addEventListener("keydown", onKey);
     52    return () => window.removeEventListener("keydown", onKey);
     53  }, [open, onClose]);
     54
     55  // load details
    2356  useEffect(() => {
    2457    if (!open || !computer?.name) return;
     
    2659
    2760    async function load() {
     61      setLoading(true);
     62      setError("");
    2863      try {
    29         setLoading(true);
    30         const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`);
    31         const d = await r.json();
     64        const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`, {
     65          credentials: "include",
     66        });
     67        const d = await r.json().catch(() => ({}));
     68        if (!r.ok) throw new Error(d?.error || `HTTP ${r.status}`);
    3269        if (!cancelled) setDetails(d);
    3370      } catch (e) {
    34         console.error("details error", e);
     71        if (!cancelled) {
     72          setError(String(e?.message || e));
     73          setDetails(null);
     74        }
    3575      } finally {
    3676        if (!cancelled) setLoading(false);
     
    5191      const msg = (ev.message || "").toLowerCase();
    5292      const type = (ev.event_type || "").toLowerCase();
    53       const id = String(ev.event_id ?? "");
     93      const id = String(ev.event_id ?? "").toLowerCase();
    5494      return msg.includes(q) || type.includes(q) || id.includes(q);
    5595    });
     
    5797
    5898  function openJson(ev) {
    59     // server ти враќа: { event_id, event_type, message, timestamp, details }
    6099    setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`);
    61     setJsonData(ev?.details ?? ev); // ако нема details, покажи цел event
     100    setJsonData(ev?.details ?? ev);
    62101    setJsonOpen(true);
    63102  }
     
    73112  return (
    74113    <>
    75       <div className="backdrop" onMouseDown={onClose}>
    76         <div className="modal" onMouseDown={(e) => e.stopPropagation()}>
    77           <div className="modal-head">
     114      <style>{`
     115        .cdm-backdrop{
     116          position: fixed; inset: 0; z-index: 60;
     117          background: rgba(2,6,23,0.70);
     118          backdrop-filter: blur(6px);
     119          display:flex; align-items:center; justify-content:center;
     120          padding: 18px;
     121        }
     122        .cdm-modal{
     123          width: min(1100px, 96vw);
     124          height: min(86vh, 860px);
     125          display:flex; flex-direction:column;
     126          border-radius: 18px;
     127          background: radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
     128                      radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
     129                      rgba(10,16,32,0.92);
     130          border: 1px solid rgba(96,165,250,0.20);
     131          box-shadow: 0 30px 80px rgba(0,0,0,0.45);
     132          overflow: hidden;
     133        }
     134        .cdm-head{
     135          padding: 14px 16px;
     136          border-bottom: 1px solid rgba(96,165,250,0.16);
     137          display:flex; align-items:center; justify-content:space-between; gap:12px;
     138        }
     139        .cdm-title{ font-size: 16px; font-weight: 900; color: rgba(255,255,255,0.95); }
     140        .cdm-sub{ margin-top: 2px; font-size: 12px; color: rgba(191,219,254,0.80); }
     141        .cdm-right{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
     142        .cdm-tabs{
     143          display:flex; gap:8px; padding: 6px;
     144          background: rgba(15,23,42,0.45);
     145          border: 1px solid rgba(96,165,250,0.18);
     146          border-radius: 999px;
     147        }
     148        .cdm-tab{
     149          appearance:none; border: 0;
     150          padding: 8px 12px;
     151          border-radius: 999px;
     152          background: transparent;
     153          color: rgba(226,232,240,0.88);
     154          font-weight: 900;
     155          font-size: 12px;
     156          cursor:pointer;
     157          transition: background .12s ease, transform .12s ease, color .12s ease;
     158        }
     159        .cdm-tab:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.55); }
     160        .cdm-tab.active{
     161          background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.65));
     162          color: rgba(5,12,24,0.95);
     163        }
     164        .cdm-btn{
     165          appearance:none;
     166          border: 1px solid rgba(96,165,250,0.22);
     167          background: rgba(15,23,42,0.55);
     168          color: rgba(255,255,255,0.92);
     169          padding: 9px 12px;
     170          border-radius: 12px;
     171          font-weight: 900;
     172          font-size: 13px;
     173          cursor: pointer;
     174          transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
     175          box-shadow: 0 8px 20px rgba(0,0,0,0.18);
     176        }
     177        .cdm-btn:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.65); border-color: rgba(96,165,250,0.38); }
     178        .cdm-btn:disabled{ opacity: .6; cursor: not-allowed; transform:none; }
     179        .cdm-btn.primary{
     180          background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.65));
     181          border-color: rgba(147,197,253,0.35);
     182          color: rgba(5,12,24,0.95);
     183        }
     184
     185        /* body scroll zone */
     186        .cdm-body{
     187          flex: 1;
     188          display:flex;
     189          flex-direction:column;
     190          min-height: 0; /* important for scroll */
     191        }
     192        .cdm-content{
     193          flex: 1;
     194          min-height: 0;
     195          overflow: auto;
     196          padding: 14px 16px 18px;
     197        }
     198
     199        .cdm-panel{
     200          border-radius: 16px;
     201          background: rgba(15,23,42,0.55);
     202          border: 1px solid rgba(96,165,250,0.16);
     203          overflow: hidden;
     204        }
     205        .cdm-panel-head{
     206          padding: 12px 12px;
     207          display:flex; align-items:center; justify-content:space-between; gap:10px;
     208          border-bottom: 1px solid rgba(96,165,250,0.14);
     209        }
     210        .cdm-panel-title{ color: rgba(255,255,255,0.92); font-weight: 900; }
     211        .cdm-panel-body{ padding: 12px; }
     212
     213        /* table area scroll fix */
     214        .cdm-table-wrap{
     215          border-radius: 14px;
     216          border: 1px solid rgba(148,163,184,0.18);
     217          background: rgba(2,6,23,0.28);
     218          overflow: hidden;
     219        }
     220        .cdm-table-scroll{
     221          max-height: 52vh;
     222          overflow: auto;
     223        }
     224        table{ width: 100%; border-collapse: collapse; }
     225        thead th{
     226          position: sticky; top: 0;
     227          background: rgba(10,16,32,0.95);
     228          backdrop-filter: blur(8px);
     229          text-align:left;
     230          font-size: 12px;
     231          color: rgba(191,219,254,0.88);
     232          padding: 10px 10px;
     233          border-bottom: 1px solid rgba(96,165,250,0.16);
     234          z-index: 1;
     235        }
     236        tbody td{
     237          padding: 10px 10px;
     238          border-bottom: 1px solid rgba(148,163,184,0.12);
     239          color: rgba(226,232,240,0.92);
     240          font-size: 13px;
     241          vertical-align: top;
     242        }
     243        tbody tr:hover td{ background: rgba(30,41,59,0.35); }
     244
     245        .cdm-pill{
     246          display:inline-flex; align-items:center; gap:8px;
     247          padding: 6px 10px;
     248          border-radius: 999px;
     249          background: rgba(30,41,59,0.55);
     250          border: 1px solid rgba(96,165,250,0.18);
     251          color: rgba(255,255,255,0.9);
     252          font-weight: 900;
     253          font-size: 12px;
     254        }
     255        .cdm-input{
     256          width: min(420px, 45vw);
     257          padding: 9px 12px;
     258          border-radius: 12px;
     259          border: 1px solid rgba(96,165,250,0.18);
     260          background: rgba(2,6,23,0.35);
     261          color: rgba(255,255,255,0.92);
     262          outline: none;
     263          font-weight: 800;
     264        }
     265        .cdm-input::placeholder{ color: rgba(148,163,184,0.75); }
     266
     267        .cdm-badge{
     268          display:inline-flex; align-items:center; justify-content:center;
     269          padding: 5px 10px;
     270          border-radius: 999px;
     271          font-weight: 1000;
     272          font-size: 11px;
     273          letter-spacing: .3px;
     274          border: 1px solid rgba(148,163,184,0.18);
     275          background: rgba(15,23,42,0.5);
     276          color: rgba(226,232,240,0.95);
     277        }
     278        .cdm-badge.high{ border-color: rgba(239,68,68,0.35); color: rgba(254,202,202,0.95); }
     279        .cdm-badge.medium{ border-color: rgba(251,191,36,0.35); color: rgba(254,243,199,0.95); }
     280        .cdm-badge.low{ border-color: rgba(34,197,94,0.25); color: rgba(220,252,231,0.95); }
     281
     282        /* JSON modal */
     283        .cdm-json-modal{
     284          width: min(980px, 96vw);
     285          height: min(82vh, 860px);
     286          display:flex; flex-direction:column;
     287          border-radius: 18px;
     288          background: rgba(10,16,32,0.94);
     289          border: 1px solid rgba(96,165,250,0.20);
     290          overflow:hidden;
     291        }
     292        .cdm-json-body{
     293          flex:1; min-height:0; overflow:auto; padding: 14px 16px 18px;
     294        }
     295        .cdm-pre{
     296          margin: 0;
     297          padding: 14px;
     298          border-radius: 12px;
     299          border: 1px solid rgba(148,163,184,0.22);
     300          background: rgba(2,6,23,0.35);
     301          color: rgba(226,232,240,0.95);
     302          font-size: 12px;
     303          line-height: 1.5;
     304          white-space: pre-wrap;
     305          word-break: break-word;
     306        }
     307      `}</style>
     308
     309      <div className="cdm-backdrop" onMouseDown={onClose}>
     310        <div className="cdm-modal" onMouseDown={(e) => e.stopPropagation()}>
     311          <div className="cdm-head">
    78312            <div>
    79               <div className="modal-title">{computer.name}</div>
    80               <div className="modal-sub">
    81                 {computer.ip || "—"} • {computer.user || "—"} • {computer.status || "—"}
     313              <div className="cdm-title">{computer?.name || "Computer"}</div>
     314              <div className="cdm-sub">
     315                {computer?.ip || "—"} • {computer?.user || "—"} • {computer?.status || "—"}
    82316              </div>
    83317            </div>
    84318
    85             <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
    86               <div className="tabs">
    87                 <button className={`tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}>
     319            <div className="cdm-right">
     320              <button className="cdm-btn primary" onClick={() => onAskAI?.(computer?.name)} disabled={!computer?.name}>
     321                Ask AI
     322              </button>
     323
     324              <div className="cdm-tabs">
     325                <button className={`cdm-tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}>
    88326                  Overview
    89327                </button>
    90                 <button className={`tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>
     328                <button className={`cdm-tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>
    91329                  Sysmon
    92330                </button>
    93                 <button className={`tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>
     331                <button className={`cdm-tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>
    94332                  Processes
    95333                </button>
    96                 <button className={`tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>
     334                <button className={`cdm-tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>
    97335                  Network
    98336                </button>
    99337              </div>
    100               <button className="btn" onClick={onClose}>
     338
     339              <button className="cdm-btn" onClick={onClose}>
    101340                Close
    102341              </button>
     
    104343          </div>
    105344
    106           <div style={{ padding: 16 }}>
    107             {loading && <div style={{ color: "#94a3b8" }}>Loading…</div>}
    108 
    109             {!loading && details && tab === "overview" && (
    110               <div className="panel">
    111                 <div className="panel-head">
    112                   <div className="panel-title">System Overview</div>
    113                   <button className="btn primary" onClick={() => onAskAI(computer.name)}>
    114                     Ask AI about this PC
    115                   </button>
     345          <div className="cdm-body">
     346            <div className="cdm-content">
     347              {loading && <div style={{ color: "rgba(148,163,184,0.95)" }}>Loading…</div>}
     348
     349              {!loading && error && (
     350                <div className="cdm-panel" style={{ borderColor: "rgba(239,68,68,0.35)" }}>
     351                  <div className="cdm-panel-head">
     352                    <div className="cdm-panel-title">Error</div>
     353                  </div>
     354                  <div className="cdm-panel-body" style={{ color: "rgba(254,202,202,0.95)" }}>
     355                    {error}
     356                  </div>
    116357                </div>
    117                 <div className="panel-body" style={{ display: "grid", gap: 10 }}>
    118                   <div>
    119                     <b>User:</b> {details.computer?.user}
    120                   </div>
    121                   <div>
    122                     <b>IP:</b> {details.computer?.ip}
    123                   </div>
    124                   <div>
    125                     <b>OS:</b> {details.computer?.os}
    126                   </div>
    127                   <div>
    128                     <b>First seen:</b> {details.computer?.first_seen}
    129                   </div>
    130                   <div>
    131                     <b>Last seen:</b> {details.computer?.last_seen}
    132                   </div>
    133                   <div>
    134                     <b>Total logs:</b> {details.computer?.total_logs}
     358              )}
     359
     360              {!loading && !error && details && tab === "overview" && (
     361                <div className="cdm-panel">
     362                  <div className="cdm-panel-head">
     363                    <div className="cdm-panel-title">System Overview</div>
     364                    <span className="cdm-pill">Env: {details.computer?.env_name || "default"}</span>
     365                  </div>
     366                  <div className="cdm-panel-body" style={{ display: "grid", gap: 10 }}>
     367                    <div><b>User:</b> {details.computer?.user || "—"}</div>
     368                    <div><b>IP:</b> {details.computer?.ip || "—"}</div>
     369                    <div><b>OS:</b> {details.computer?.os || "—"}</div>
     370                    <div><b>First seen:</b> {details.computer?.first_seen || "—"}</div>
     371                    <div><b>Last seen:</b> {details.computer?.last_seen || "—"}</div>
     372                    <div><b>Total logs:</b> {details.computer?.total_logs ?? "—"}</div>
    135373                  </div>
    136374                </div>
    137               </div>
    138             )}
    139 
    140             {!loading && details && tab === "sysmon" && (
    141               <div className="panel">
    142                 <div className="panel-head" style={{ alignItems: "center" }}>
    143                   <div className="panel-title">Sysmon Events</div>
    144                   <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
    145                     <span className="pill">{filteredSysmon.length} events</span>
    146                     <input
    147                       className="input"
    148                       style={{ maxWidth: 380 }}
    149                       placeholder="Search by id / type / message…"
    150                       value={search}
    151                       onChange={(e) => setSearch(e.target.value)}
    152                     />
     375              )}
     376
     377              {!loading && !error && details && tab === "sysmon" && (
     378                <div className="cdm-panel">
     379                  <div className="cdm-panel-head">
     380                    <div className="cdm-panel-title">Sysmon Events</div>
     381                    <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
     382                      <span className="cdm-pill">{filteredSysmon.length} events</span>
     383                      <input
     384                        className="cdm-input"
     385                        placeholder="Search by id / type / message…"
     386                        value={search}
     387                        onChange={(e) => setSearch(e.target.value)}
     388                      />
     389                    </div>
     390                  </div>
     391
     392                  <div className="cdm-panel-body">
     393                    <div className="cdm-table-wrap">
     394                      <div className="cdm-table-scroll">
     395                        <table>
     396                          <thead>
     397                            <tr>
     398                              <th style={{ width: 90 }}>Time</th>
     399                              <th style={{ width: 70 }}>ID</th>
     400                              <th style={{ width: 190 }}>Type</th>
     401                              <th style={{ width: 110 }}>Severity</th>
     402                              <th>Message</th>
     403                              <th style={{ width: 90 }}>View</th>
     404                            </tr>
     405                          </thead>
     406                          <tbody>
     407                            {filteredSysmon.slice(0, 400).map((ev, idx) => {
     408                              const sev = severityForEventId(ev.event_id);
     409                              return (
     410                                <tr key={idx}>
     411                                  <td>{fmtTime(ev.timestamp)}</td>
     412                                  <td><b>{ev.event_id ?? "—"}</b></td>
     413                                  <td>{ev.event_type || "—"}</td>
     414                                  <td><span className={`cdm-badge ${sev}`}>{sev.toUpperCase()}</span></td>
     415                                  <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
     416                                    {(ev.message || "").slice(0, 220)}
     417                                  </td>
     418                                  <td>
     419                                    <button className="cdm-btn" onClick={() => openJson(ev)}>
     420                                      View
     421                                    </button>
     422                                  </td>
     423                                </tr>
     424                              );
     425                            })}
     426
     427                            {filteredSysmon.length === 0 && (
     428                              <tr>
     429                                <td colSpan="6" style={{ color: "rgba(148,163,184,0.95)" }}>
     430                                  No sysmon events.
     431                                </td>
     432                              </tr>
     433                            )}
     434                          </tbody>
     435                        </table>
     436                      </div>
     437                    </div>
     438                    {filteredSysmon.length > 400 && (
     439                      <div style={{ marginTop: 10, color: "rgba(148,163,184,0.95)", fontSize: 12 }}>
     440                        Showing first 400 (for speed).
     441                      </div>
     442                    )}
    153443                  </div>
    154444                </div>
    155 
    156                 <div className="panel-body">
    157                   <div className="table-wrap">
    158                     <table>
    159                       <thead>
    160                         <tr>
    161                           <th style={{ width: 90 }}>Time</th>
    162                           <th style={{ width: 70 }}>ID</th>
    163                           <th style={{ width: 170 }}>Type</th>
    164                           <th style={{ width: 110 }}>Severity</th>
    165                           <th>Message</th>
    166                           <th style={{ width: 90 }}>View</th>
    167                         </tr>
    168                       </thead>
    169                       <tbody>
    170                         {filteredSysmon.slice(0, 200).map((ev, idx) => {
    171                           const sev = severityForEventId(ev.event_id);
    172                           return (
    173                             <tr key={idx}>
    174                               <td>{ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : "—"}</td>
    175                               <td>
    176                                 <b>{ev.event_id}</b>
    177                               </td>
    178                               <td>{ev.event_type}</td>
    179                               <td>
    180                                 <span className={`badge ${sev}`}>{sev.toUpperCase()}</span>
    181                               </td>
    182                               <td
    183                                 style={{
    184                                   fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    185                                   fontSize: 12,
    186                                 }}
    187                               >
    188                                 {(ev.message || "").slice(0, 140)}
    189                               </td>
    190                               <td>
    191                                 <button className="btn" onClick={() => openJson(ev)}>
    192                                   View
    193                                 </button>
    194                               </td>
     445              )}
     446
     447              {!loading && !error && details && tab === "processes" && (
     448                <div className="cdm-panel">
     449                  <div className="cdm-panel-head">
     450                    <div className="cdm-panel-title">Recent Processes</div>
     451                    <span className="cdm-pill">{(details.recent_processes || []).length} rows</span>
     452                  </div>
     453
     454                  <div className="cdm-panel-body">
     455                    <div className="cdm-table-wrap">
     456                      <div className="cdm-table-scroll">
     457                        <table>
     458                          <thead>
     459                            <tr>
     460                              <th style={{ width: 90 }}>Time</th>
     461                              <th style={{ width: 80 }}>PID</th>
     462                              <th style={{ width: 200 }}>Name</th>
     463                              <th style={{ width: 140 }}>User</th>
     464                              <th style={{ width: 90 }}>CPU%</th>
     465                              <th style={{ width: 110 }}>RAM MB</th>
     466                              <th>Cmdline</th>
    195467                            </tr>
    196                           );
    197                         })}
    198 
    199                         {filteredSysmon.length === 0 && (
    200                           <tr>
    201                             <td colSpan="6" style={{ color: "#94a3b8" }}>
    202                               No sysmon events.
    203                             </td>
    204                           </tr>
    205                         )}
    206                       </tbody>
    207                     </table>
    208                   </div>
    209 
    210                   {filteredSysmon.length > 200 && (
    211                     <div style={{ marginTop: 10, color: "#94a3b8", fontSize: 12 }}>
    212                       Showing first 200 (for speed).
     468                          </thead>
     469                          <tbody>
     470                            {(details.recent_processes || []).slice(0, 400).map((p, idx) => (
     471                              <tr key={idx}>
     472                                <td>{fmtTime(p.timestamp)}</td>
     473                                <td><b>{p.pid ?? "—"}</b></td>
     474                                <td>{p.name || "—"}</td>
     475                                <td>{p.username || "—"}</td>
     476                                <td>{p.cpu_percent ?? 0}</td>
     477                                <td>{p.memory_mb ?? 0}</td>
     478                                <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
     479                                  {(p.cmdline || "").slice(0, 220)}
     480                                </td>
     481                              </tr>
     482                            ))}
     483                            {(details.recent_processes || []).length === 0 && (
     484                              <tr>
     485                                <td colSpan="7" style={{ color: "rgba(148,163,184,0.95)" }}>
     486                                  No process data.
     487                                </td>
     488                              </tr>
     489                            )}
     490                          </tbody>
     491                        </table>
     492                      </div>
    213493                    </div>
    214                   )}
     494                  </div>
    215495                </div>
    216               </div>
    217             )}
    218 
    219             {!loading && details && tab === "processes" && (
    220               <div className="panel">
    221                 <div className="panel-head">
    222                   <div className="panel-title">Recent Processes</div>
     496              )}
     497
     498              {!loading && !error && details && tab === "network" && (
     499                <div className="cdm-panel">
     500                  <div className="cdm-panel-head">
     501                    <div className="cdm-panel-title">Network Connections</div>
     502                    <span className="cdm-pill">{(details.network_connections || []).length} rows</span>
     503                  </div>
     504
     505                  <div className="cdm-panel-body">
     506                    <div className="cdm-table-wrap">
     507                      <div className="cdm-table-scroll">
     508                        <table>
     509                          <thead>
     510                            <tr>
     511                              <th style={{ width: 90 }}>Time</th>
     512                              <th style={{ width: 80 }}>PID</th>
     513                              <th style={{ width: 180 }}>Process</th>
     514                              <th style={{ width: 220 }}>Local</th>
     515                              <th style={{ width: 220 }}>Remote</th>
     516                              <th style={{ width: 140 }}>Status</th>
     517                            </tr>
     518                          </thead>
     519                          <tbody>
     520                            {(details.network_connections || []).slice(0, 400).map((n, idx) => (
     521                              <tr key={idx}>
     522                                <td>{fmtTime(n.timestamp)}</td>
     523                                <td><b>{n.pid ?? "—"}</b></td>
     524                                <td>{n.process_name || "—"}</td>
     525                                <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
     526                                  {n.local_address || "—"}
     527                                </td>
     528                                <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
     529                                  {n.remote_address || "—"}
     530                                </td>
     531                                <td>{n.status || "—"}</td>
     532                              </tr>
     533                            ))}
     534                            {(details.network_connections || []).length === 0 && (
     535                              <tr>
     536                                <td colSpan="6" style={{ color: "rgba(148,163,184,0.95)" }}>
     537                                  No network data.
     538                                </td>
     539                              </tr>
     540                            )}
     541                          </tbody>
     542                        </table>
     543                      </div>
     544                    </div>
     545                  </div>
    223546                </div>
    224                 <div className="panel-body">
    225                   <div className="table-wrap">
    226                     <table>
    227                       <thead>
    228                         <tr>
    229                           <th>Time</th>
    230                           <th>PID</th>
    231                           <th>Name</th>
    232                           <th>User</th>
    233                           <th>CPU%</th>
    234                           <th>RAM MB</th>
    235                           <th>Cmdline</th>
    236                         </tr>
    237                       </thead>
    238                       <tbody>
    239                         {(details.recent_processes || []).slice(0, 150).map((p, idx) => (
    240                           <tr key={idx}>
    241                             <td>{p.timestamp ? new Date(p.timestamp).toLocaleTimeString() : "—"}</td>
    242                             <td>
    243                               <b>{p.pid}</b>
    244                             </td>
    245                             <td>{p.name}</td>
    246                             <td>{p.username || "—"}</td>
    247                             <td>{p.cpu_percent ?? 0}</td>
    248                             <td>{p.memory_mb ?? 0}</td>
    249                             <td
    250                               style={{
    251                                 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    252                                 fontSize: 12,
    253                               }}
    254                             >
    255                               {(p.cmdline || "").slice(0, 120)}
    256                             </td>
    257                           </tr>
    258                         ))}
    259                         {(details.recent_processes || []).length === 0 && (
    260                           <tr>
    261                             <td colSpan="7" style={{ color: "#94a3b8" }}>
    262                               No process data.
    263                             </td>
    264                           </tr>
    265                         )}
    266                       </tbody>
    267                     </table>
    268                   </div>
    269                 </div>
    270               </div>
    271             )}
    272 
    273             {!loading && details && tab === "network" && (
    274               <div className="panel">
    275                 <div className="panel-head">
    276                   <div className="panel-title">Network Connections</div>
    277                 </div>
    278                 <div className="panel-body">
    279                   <div className="table-wrap">
    280                     <table>
    281                       <thead>
    282                         <tr>
    283                           <th>Time</th>
    284                           <th>PID</th>
    285                           <th>Process</th>
    286                           <th>Local</th>
    287                           <th>Remote</th>
    288                           <th>Status</th>
    289                         </tr>
    290                       </thead>
    291                       <tbody>
    292                         {(details.network_connections || []).slice(0, 150).map((n, idx) => (
    293                           <tr key={idx}>
    294                             <td>{n.timestamp ? new Date(n.timestamp).toLocaleTimeString() : "—"}</td>
    295                             <td>
    296                               <b>{n.pid}</b>
    297                             </td>
    298                             <td>{n.process_name || "—"}</td>
    299                             <td
    300                               style={{
    301                                 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    302                                 fontSize: 12,
    303                               }}
    304                             >
    305                               {n.local_address || "—"}
    306                             </td>
    307                             <td
    308                               style={{
    309                                 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    310                                 fontSize: 12,
    311                               }}
    312                             >
    313                               {n.remote_address || "—"}
    314                             </td>
    315                             <td>{n.status || "—"}</td>
    316                           </tr>
    317                         ))}
    318                         {(details.network_connections || []).length === 0 && (
    319                           <tr>
    320                             <td colSpan="6" style={{ color: "#94a3b8" }}>
    321                               No network data.
    322                             </td>
    323                           </tr>
    324                         )}
    325                       </tbody>
    326                     </table>
    327                   </div>
    328                 </div>
    329               </div>
    330             )}
    331 
    332             {!loading && !details && <div style={{ color: "#94a3b8" }}>No details.</div>}
     547              )}
     548
     549              {!loading && !error && !details && (
     550                <div style={{ color: "rgba(148,163,184,0.95)" }}>No details.</div>
     551              )}
     552            </div>
    333553          </div>
    334554        </div>
    335555      </div>
    336556
    337       {/* JSON VIEWER MODAL */}
     557      {/* JSON VIEWER */}
    338558      {jsonOpen && (
    339         <div className="backdrop" onMouseDown={closeJson}>
    340           <div className="modal" onMouseDown={(e) => e.stopPropagation()} style={{ width: "min(900px, 96vw)" }}>
    341             <div className="modal-head">
     559        <div className="cdm-backdrop" onMouseDown={closeJson}>
     560          <div className="cdm-json-modal" onMouseDown={(e) => e.stopPropagation()}>
     561            <div className="cdm-head">
    342562              <div>
    343                 <div className="modal-title">View JSON</div>
    344                 <div className="modal-sub">{jsonTitle}</div>
     563                <div className="cdm-title">View JSON</div>
     564                <div className="cdm-sub">{jsonTitle}</div>
    345565              </div>
    346               <div style={{ display: "flex", gap: 10 }}>
     566              <div className="cdm-right">
    347567                <button
    348                   className="btn"
    349                   onClick={() => {
    350                     const txt = JSON.stringify(jsonData ?? {}, null, 2);
    351                     navigator.clipboard.writeText(txt);
    352                   }}
     568                  className="cdm-btn"
     569                  onClick={() => navigator.clipboard.writeText(JSON.stringify(jsonData ?? {}, null, 2))}
    353570                >
    354571                  Copy
    355572                </button>
    356                 <button className="btn" onClick={closeJson}>
     573                <button className="cdm-btn" onClick={closeJson}>
    357574                  Close
    358575                </button>
    359576              </div>
    360577            </div>
    361             <div style={{ padding: 16 }}>
    362               <pre
    363                 style={{
    364                   margin: 0,
    365                   maxHeight: "70vh",
    366                   overflow: "auto",
    367                   padding: 14,
    368                   borderRadius: 12,
    369                   border: "1px solid rgba(148,163,184,0.25)",
    370                   background: "rgba(2,6,23,0.35)",
    371                   color: "rgba(226,232,240,0.95)",
    372                   fontSize: 12,
    373                   lineHeight: 1.5,
    374                 }}
    375               >
    376                 {JSON.stringify(jsonData ?? {}, null, 2)}
    377               </pre>
     578
     579            <div className="cdm-json-body">
     580              <pre className="cdm-pre">{JSON.stringify(jsonData ?? {}, null, 2)}</pre>
    378581            </div>
    379582          </div>
  • lan-frontend/src/components/TopBar.jsx

    r2058e5c r505f39a  
    11import React, { useEffect, useMemo, useState } from "react";
    2 
    3 export default function TopBar({ onOpenAI, onRefresh, lastUpdated, onEnvChange }) {
     2import logo from "../assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png";
     3
     4export default function TopBar({
     5  onOpenAI,
     6  onRefresh,
     7  lastUpdated,
     8  onEnvChange,
     9  selectedEnv: selectedEnvProp, // optional (ако сакаш да го контролира App)
     10  me,
     11  onLoggedOut, // optional callback (App loadMe)
     12}) {
    413  const [open, setOpen] = useState(false);
    5 
    6   // admin auth
    7   const [adminKey, setAdminKey] = useState("");
    8   const [adminSession, setAdminSession] = useState("");
    914
    1015  // env + tokens
    1116  const [envs, setEnvs] = useState(["default"]);
    12   const [selectedEnv, setSelectedEnv] = useState("default");
     17  const [selectedEnv, setSelectedEnv] = useState(selectedEnvProp || "default");
    1318  const [newEnvName, setNewEnvName] = useState("");
    1419  const [token, setToken] = useState("");
     
    1722  const [error, setError] = useState("");
    1823
    19   // helper: fetch env list after login
    20   async function loadEnvironments(session) {
     24
     25  // keep in sync ако App праќа selectedEnv prop
     26  useEffect(() => {
     27    if (selectedEnvProp && selectedEnvProp !== selectedEnv) {
     28      setSelectedEnv(selectedEnvProp);
     29    }
     30    // eslint-disable-next-line react-hooks/exhaustive-deps
     31  }, [selectedEnvProp]);
     32
     33  async function loadEnvironments() {
    2134    const r = await fetch("/api/admin/environments", {
    22       headers: { "X-Admin-Session": session },
     35      credentials: "include",
    2336    });
    2437    const data = await r.json();
     
    2841    setEnvs(list);
    2942
    30     // ако тековниот не постои, стави првиот од листата
    3143    if (!list.includes(selectedEnv)) {
    3244      setSelectedEnv(list[0]);
     
    3547  }
    3648
    37   async function login() {
    38     setError("");
    39     setToken("");
    40 
    41     if (!adminKey.trim()) {
    42       setError("Внеси admin key.");
    43       return;
    44     }
    45 
    46     setBusy(true);
    47     try {
    48       const r = await fetch("/api/admin/login", {
    49         method: "POST",
    50         headers: { "Content-Type": "application/json" },
    51         body: JSON.stringify({ admin_key: adminKey.trim() }),
    52       });
    53       const data = await r.json();
    54       if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
    55 
    56       const session = data.session;
    57       setAdminSession(session);
    58       await loadEnvironments(session);
    59     } catch (e) {
    60       setError(String(e.message || e));
    61     } finally {
    62       setBusy(false);
    63     }
    64   }
    65 
    66   function logout() {
    67     setAdminSession("");
    68     setAdminKey("");
    69     setToken("");
    70     setError("");
    71     setEnvs(["default"]);
    72     setSelectedEnv("default");
    73     onEnvChange?.("default");
    74   }
    75 
    7649  async function createEnvironment() {
    7750    setError("");
    7851    setToken("");
     52
    7953    if (!newEnvName.trim()) {
    8054      setError("Внеси име за environment.");
     
    8660      const r = await fetch("/api/admin/environments", {
    8761        method: "POST",
    88         headers: {
    89           "Content-Type": "application/json",
    90           "X-Admin-Session": adminSession,
    91         },
     62        headers: { "Content-Type": "application/json" },
     63        credentials: "include",
    9264        body: JSON.stringify({ name: newEnvName.trim() }),
    9365      });
     
    9668      if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
    9769
    98       // reload list, select new env
    99       await loadEnvironments(adminSession);
     70      await loadEnvironments();
    10071      setSelectedEnv(newEnvName.trim());
    10172      onEnvChange?.(newEnvName.trim());
     
    12091      const r = await fetch("/api/admin/tokens", {
    12192        method: "POST",
    122         headers: {
    123           "Content-Type": "application/json",
    124           "X-Admin-Session": adminSession,
    125         },
     93        headers: { "Content-Type": "application/json" },
     94        credentials: "include",
    12695        body: JSON.stringify({ env: selectedEnv }),
    12796      });
     
    147116    setToken("");
    148117    onEnvChange?.(val);
     118  }
     119
     120  async function logout() {
     121    try {
     122      await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
     123    } catch {}
     124    onLoggedOut?.();
     125    // reset local admin modal state
     126    setOpen(false);
     127    setToken("");
     128    setError("");
    149129  }
    150130
     
    161141    <>
    162142      <style>{`
    163         /* ---- TopBar scoped theme ---- */
    164         .tb-wrap{
    165           position: sticky;
    166           top: 0;
    167           z-index: 20;
    168           backdrop-filter: blur(10px);
     143        .tb-wrap{ position: sticky; top: 0; z-index: 20; backdrop-filter: blur(10px);
    169144          background: linear-gradient(180deg, rgba(10,16,32,0.92), rgba(10,16,32,0.75));
    170           border-bottom: 1px solid rgba(96,165,250,0.18);
    171         }
    172         .tb-inner{
    173           max-width: 1200px;
    174           margin: 0 auto;
    175           padding: 14px 18px;
    176           display: flex;
    177           align-items: center;
    178           justify-content: space-between;
    179           gap: 14px;
    180         }
    181 
    182         .tb-brand{
    183           display:flex;
    184           align-items:center;
    185           gap: 12px;
    186           min-width: 260px;
    187         }
    188         .tb-badge{
    189           width: 44px;
    190           height: 44px;
    191           border-radius: 14px;
    192           display:flex;
    193           align-items:center;
    194           justify-content:center;
    195           background:
    196             radial-gradient(120px 80px at 30% 20%, rgba(96,165,250,0.55), transparent 60%),
    197             linear-gradient(135deg, rgba(30,64,175,0.75), rgba(15,23,42,0.85));
    198           border: 1px solid rgba(96,165,250,0.25);
    199           box-shadow: 0 10px 25px rgba(0,0,0,0.25);
    200         }
    201         .tb-title{
    202           margin:0;
    203           font-size: 16px;
    204           font-weight: 900;
    205           letter-spacing: 0.4px;
    206           color: rgba(255,255,255,0.95);
    207           line-height: 1.2;
    208         }
    209         .tb-sub{
    210           margin: 2px 0 0 0;
    211           font-size: 12px;
    212           color: rgba(191,219,254,0.85);
    213         }
    214         .tb-sub span{
    215           color: rgba(96,165,250,0.95);
    216           font-weight: 800;
    217         }
    218 
    219         .tb-actions{
    220           display:flex;
    221           align-items:center;
    222           gap: 10px;
    223           flex-wrap: wrap;
    224           justify-content: flex-end;
    225         }
    226 
    227         .tb-pill{
    228           display:inline-flex;
    229           align-items:center;
    230           gap: 8px;
    231           padding: 8px 12px;
    232           border-radius: 999px;
    233           background: rgba(30,41,59,0.65);
    234           border: 1px solid rgba(96,165,250,0.22);
    235           color: rgba(255,255,255,0.9);
    236           font-weight: 800;
    237           font-size: 12px;
    238           box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
    239         }
    240         .tb-pill .dot{
    241           width: 8px;
    242           height: 8px;
    243           border-radius: 999px;
    244           background: rgba(96,165,250,0.95);
    245           box-shadow: 0 0 0 3px rgba(96,165,250,0.15);
    246         }
    247 
    248         .tb-btn{
    249           appearance: none;
    250           border: 1px solid rgba(96,165,250,0.22);
    251           background: rgba(15,23,42,0.55);
    252           color: rgba(255,255,255,0.9);
    253           padding: 9px 12px;
    254           border-radius: 12px;
    255           font-weight: 800;
    256           font-size: 13px;
    257           cursor: pointer;
    258           transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
    259           box-shadow: 0 8px 20px rgba(0,0,0,0.18);
    260         }
    261         .tb-btn:hover{
    262           transform: translateY(-1px);
    263           background: rgba(30,41,59,0.65);
    264           border-color: rgba(96,165,250,0.38);
    265         }
    266         .tb-btn:disabled{
    267           opacity: .6;
    268           cursor: not-allowed;
    269           transform: none;
    270         }
    271         .tb-btn.primary{
    272           background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.7));
    273           border-color: rgba(147,197,253,0.35);
    274           color: rgba(5,12,24,0.95);
    275         }
    276         .tb-btn.primary:hover{
    277           background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75));
    278         }
    279 
    280         /* ---- Modal ---- */
    281         .tb-backdrop{
    282           position: fixed;
    283           inset: 0;
    284           background: rgba(2,6,23,0.72);
    285           backdrop-filter: blur(6px);
    286           display:flex;
    287           align-items: center;
    288           justify-content: center;
    289           padding: 20px;
    290           z-index: 50;
    291         }
    292         .tb-modal{
    293           width: min(860px, 96vw);
    294           border-radius: 18px;
    295           background:
    296             radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
    297             radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
    298             rgba(10,16,32,0.92);
    299           border: 1px solid rgba(96,165,250,0.22);
    300           box-shadow: 0 30px 80px rgba(0,0,0,0.45);
    301           overflow: hidden;
    302         }
    303         .tb-modal-head{
    304           padding: 16px 18px;
    305           display:flex;
    306           align-items: center;
    307           justify-content: space-between;
    308           gap: 10px;
    309           border-bottom: 1px solid rgba(96,165,250,0.18);
    310         }
    311         .tb-modal-title{
    312           color: rgba(255,255,255,0.96);
    313           font-weight: 900;
    314           letter-spacing: .3px;
    315         }
    316         .tb-modal-sub{
    317           color: rgba(191,219,254,0.8);
    318           font-size: 12px;
    319           margin-top: 2px;
    320         }
    321         .tb-grid{
    322           padding: 16px;
    323           display:grid;
    324           gap: 12px;
    325         }
    326         .tb-card{
    327           border-radius: 16px;
    328           background: rgba(15,23,42,0.55);
    329           border: 1px solid rgba(96,165,250,0.18);
    330           padding: 14px;
    331         }
    332         .tb-card-head{
    333           display:flex;
    334           align-items:center;
    335           justify-content: space-between;
    336           margin-bottom: 10px;
    337         }
    338         .tb-card-title{
    339           color: rgba(255,255,255,0.92);
    340           font-weight: 900;
    341         }
    342         .tb-help{
    343           color: rgba(148,163,184,0.95);
    344           font-size: 12px;
    345           margin-top: 8px;
    346           line-height: 1.35;
    347         }
    348         .tb-input, .tb-select{
    349           width: 100%;
    350           padding: 10px 12px;
    351           border-radius: 12px;
    352           border: 1px solid rgba(96,165,250,0.18);
    353           background: rgba(2,6,23,0.35);
    354           color: rgba(255,255,255,0.92);
    355           outline: none;
    356           font-weight: 700;
    357         }
     145          border-bottom: 1px solid rgba(96,165,250,0.18); }
     146        .tb-inner{ max-width: 1200px; margin: 0 auto; padding: 14px 18px;
     147          display: flex; align-items: center; justify-content: space-between; gap: 14px; }
     148        .tb-brand{ display:flex; align-items:center; gap: 12px; min-width: 260px; }
     149        .tb-badge{ width: 44px; height: 44px; border-radius: 14px; display:flex; align-items:center; justify-content:center;
     150          background: radial-gradient(120px 80px at 30% 20%, rgba(96,165,250,0.55), transparent 60%),
     151                      linear-gradient(135deg, rgba(30,64,175,0.75), rgba(15,23,42,0.85));
     152          border: 1px solid rgba(96,165,250,0.25); box-shadow: 0 10px 25px rgba(0,0,0,0.25); }
     153        .tb-title{ margin:0; font-size: 16px; font-weight: 900; letter-spacing: 0.4px; color: rgba(255,255,255,0.95); line-height: 1.2; }
     154        .tb-sub{ margin: 2px 0 0 0; font-size: 12px; color: rgba(191,219,254,0.85); }
     155        .tb-sub span{ color: rgba(96,165,250,0.95); font-weight: 800; }
     156        .tb-actions{ display:flex; align-items:center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
     157        .tb-pill{ display:inline-flex; align-items:center; gap: 8px; padding: 8px 12px; border-radius: 999px;
     158          background: rgba(30,41,59,0.65); border: 1px solid rgba(96,165,250,0.22);
     159          color: rgba(255,255,255,0.9); font-weight: 800; font-size: 12px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.06); }
     160        .tb-pill .dot{ width: 8px; height: 8px; border-radius: 999px; background: rgba(96,165,250,0.95);
     161          box-shadow: 0 0 0 3px rgba(96,165,250,0.15); }
     162        .tb-btn{ appearance: none; border: 1px solid rgba(96,165,250,0.22); background: rgba(15,23,42,0.55);
     163          color: rgba(255,255,255,0.9); padding: 9px 12px; border-radius: 12px; font-weight: 800;
     164          font-size: 13px; cursor: pointer; transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
     165          box-shadow: 0 8px 20px rgba(0,0,0,0.18); }
     166        .tb-btn:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.65); border-color: rgba(96,165,250,0.38); }
     167        .tb-btn:disabled{ opacity: .6; cursor: not-allowed; transform: none; }
     168        .tb-btn.primary{ background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.7));
     169          border-color: rgba(147,197,253,0.35); color: rgba(5,12,24,0.95); }
     170        .tb-btn.primary:hover{ background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75)); }
     171
     172        .tb-backdrop{ position: fixed; inset: 0; background: rgba(2,6,23,0.72); backdrop-filter: blur(6px);
     173          display:flex; align-items: center; justify-content: center; padding: 20px; z-index: 50; }
     174        .tb-modal{ width: min(860px, 96vw); border-radius: 18px;
     175          background: radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
     176                      radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
     177                      rgba(10,16,32,0.92);
     178          border: 1px solid rgba(96,165,250,0.22); box-shadow: 0 30px 80px rgba(0,0,0,0.45); overflow: hidden; }
     179        .tb-modal-head{ padding: 16px 18px; display:flex; align-items: center; justify-content: space-between; gap: 10px;
     180          border-bottom: 1px solid rgba(96,165,250,0.18); }
     181        .tb-modal-title{ color: rgba(255,255,255,0.96); font-weight: 900; letter-spacing: .3px; }
     182        .tb-modal-sub{ color: rgba(191,219,254,0.8); font-size: 12px; margin-top: 2px; }
     183        .tb-grid{ padding: 16px; display:grid; gap: 12px; }
     184        .tb-card{ border-radius: 16px; background: rgba(15,23,42,0.55); border: 1px solid rgba(96,165,250,0.18); padding: 14px; }
     185        .tb-card-head{ display:flex; align-items:center; justify-content: space-between; margin-bottom: 10px; gap: 10px; }
     186        .tb-card-title{ color: rgba(255,255,255,0.92); font-weight: 900; }
     187        .tb-help{ color: rgba(148,163,184,0.95); font-size: 12px; margin-top: 8px; line-height: 1.35; }
     188        .tb-input, .tb-select{ width: 100%; padding: 10px 12px; border-radius: 12px;
     189          border: 1px solid rgba(96,165,250,0.18); background: rgba(2,6,23,0.35); color: rgba(255,255,255,0.92);
     190          outline: none; font-weight: 700; }
    358191        .tb-input::placeholder{ color: rgba(148,163,184,0.75); }
    359         .tb-row{
    360           display:grid;
    361           gap: 10px;
    362         }
    363         .tb-token{
    364           display:flex;
    365           align-items:center;
    366           justify-content: space-between;
    367           gap: 10px;
    368           padding: 10px 12px;
    369           border-radius: 14px;
    370           background: rgba(2,6,23,0.38);
    371           border: 1px dashed rgba(147,197,253,0.32);
    372           color: rgba(255,255,255,0.92);
    373         }
    374         .tb-mono{
    375           font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    376           font-weight: 800;
    377           color: rgba(191,219,254,0.95);
    378         }
    379         .tb-error{
    380           border-color: rgba(239,68,68,0.45) !important;
    381           background: rgba(127,29,29,0.15) !important;
    382           color: rgba(254,202,202,0.95);
    383         }
     192        .tb-row{ display:grid; gap: 10px; }
     193        .tb-token{ display:flex; align-items:center; justify-content: space-between; gap: 10px; padding: 10px 12px;
     194          border-radius: 14px; background: rgba(2,6,23,0.38); border: 1px dashed rgba(147,197,253,0.32);
     195          color: rgba(255,255,255,0.92); }
     196        .tb-mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
     197          font-weight: 800; color: rgba(191,219,254,0.95); }
     198        .tb-error{ border-color: rgba(239,68,68,0.45) !important; background: rgba(127,29,29,0.15) !important;
     199          color: rgba(254,202,202,0.95); }
    384200      `}</style>
    385201
    386       {/* TOPBAR */}
    387202      <div className="tb-wrap">
    388203        <div className="tb-inner">
     
    394209                Sysmon Edition • <span>{prettyTime}</span>
    395210              </p>
     211              {me?.email && (
     212                <p className="tb-sub" style={{ marginTop: 4, opacity: 0.9 }}>
     213                  Signed in as <span>{me.email}</span>
     214                  {me.role ? ` • ${me.role}` : ""}
     215                </p>
     216              )}
    396217            </div>
    397218          </div>
     219          <div>
     220            <img
     221                src={logo}
     222                alt="NETIntel logo"
     223                className="netintel-logo"
     224                width={150}
     225                style={{marginTop: 10 }}
     226            />
     227          </div>
     228
     229
    398230
    399231          <div className="tb-actions">
     
    402234              Env: {selectedEnv}
    403235            </span>
    404             <button className="tb-btn" onClick={onRefresh} disabled={busy}>Refresh</button>
    405             <button className="tb-btn" onClick={() => setOpen(true)}>Admin</button>
    406             <button className="tb-btn primary" onClick={onOpenAI}>AI Assistant</button>
     236            <button className="tb-btn" onClick={onRefresh} disabled={busy}>
     237              Refresh
     238            </button>
     239            <button
     240              className="tb-btn"
     241              onClick={async () => {
     242                setOpen(true);
     243                setError("");
     244                setToken("");
     245                try {
     246                  await loadEnvironments();
     247                } catch (e) {
     248                  setError(String(e.message || e));
     249                }
     250              }}
     251            >
     252              Admin
     253            </button>
     254            <button className="tb-btn primary" onClick={onOpenAI}>
     255              AI Assistant
     256            </button>
    407257          </div>
    408258        </div>
    409259      </div>
    410260
    411       {/* MODAL */}
    412261      {open && (
    413262        <div className="tb-backdrop" onMouseDown={() => setOpen(false)}>
     
    415264            <div className="tb-modal-head">
    416265              <div>
    417                 <div className="tb-modal-title">Admin Panel</div>
    418                 <div className="tb-modal-sub">Login ➜ Environments ➜ Generate token</div>
    419               </div>
    420               <button className="tb-btn" onClick={() => setOpen(false)}>Close</button>
     266                <div className="tb-modal-title">Tenant Admin</div>
     267                <div className="tb-modal-sub">
     268                  Environments ➜ Generate token (clients send X-Env-Token to /receive)
     269                </div>
     270              </div>
     271              <div style={{ display: "flex", gap: 10 }}>
     272                <button className="tb-btn" onClick={logout}>
     273                  Logout
     274                </button>
     275                <button className="tb-btn" onClick={() => setOpen(false)}>
     276                  Close
     277                </button>
     278              </div>
    421279            </div>
    422280
    423281            <div className="tb-grid">
    424               {/* 1) LOGIN */}
    425               {!adminSession ? (
    426                 <div className="tb-card">
    427                   <div className="tb-card-head">
    428                     <div className="tb-card-title">Step 1: Admin login</div>
     282              <div className="tb-card">
     283                <div className="tb-card-head">
     284                  <div className="tb-card-title">Choose environment</div>
     285                </div>
     286                <div className="tb-row">
     287                  <select
     288                    className="tb-select"
     289                    value={selectedEnv}
     290                    onChange={(e) => changeEnv(e.target.value)}
     291                  >
     292                    {envs.map((e) => (
     293                      <option key={e} value={e}>
     294                        {e}
     295                      </option>
     296                    ))}
     297                  </select>
     298                  <div className="tb-help">
     299                    Овој env ќе се користи за dashboard филтрирање + token generation.
    429300                  </div>
    430 
    431                   <div className="tb-row">
    432                     <input
    433                       className="tb-input"
    434                       type="password"
    435                       value={adminKey}
    436                       onChange={(e) => setAdminKey(e.target.value)}
    437                       placeholder="Enter admin key"
    438                     />
    439                     <button className="tb-btn primary" onClick={login} disabled={busy}>
    440                       {busy ? "Logging in…" : "Login"}
    441                     </button>
    442                     <div className="tb-help">
    443                       Admin key е само за логирање. После тоа користиме <b>session token</b>.
    444                     </div>
    445                   </div>
    446                 </div>
    447               ) : (
    448                 <>
    449                   {/* 2) ENV SELECT */}
    450                   <div className="tb-card">
    451                     <div className="tb-card-head">
    452                       <div className="tb-card-title">Step 2: Choose environment</div>
    453                       <button className="tb-btn" onClick={logout}>Logout</button>
    454                     </div>
    455 
    456                     <div className="tb-row">
    457                       <select className="tb-select" value={selectedEnv} onChange={(e) => changeEnv(e.target.value)}>
    458                         {envs.map((e) => (
    459                           <option key={e} value={e}>{e}</option>
    460                         ))}
    461                       </select>
     301                </div>
     302              </div>
     303
     304              <div className="tb-card">
     305                <div className="tb-card-head">
     306                  <div className="tb-card-title">Create new environment</div>
     307                </div>
     308                <div className="tb-row">
     309                  <input
     310                    className="tb-input"
     311                    value={newEnvName}
     312                    onChange={(e) => setNewEnvName(e.target.value)}
     313                    placeholder="e.g. school-lab / staging"
     314                  />
     315                  <button className="tb-btn" onClick={createEnvironment} disabled={busy}>
     316                    {busy ? "Creating…" : "Create"}
     317                  </button>
     318                </div>
     319              </div>
     320
     321              <div className="tb-card">
     322                <div className="tb-card-head">
     323                  <div className="tb-card-title">Generate token (for clients)</div>
     324                </div>
     325
     326                <div className="tb-row">
     327                  <button className="tb-btn primary" onClick={generateToken} disabled={busy}>
     328                    {busy ? "Generating…" : `Generate token for "${selectedEnv}"`}
     329                  </button>
     330
     331                  {token && (
     332                    <>
     333                      <div className="tb-token">
     334                        <div>
     335                          <div style={{ fontWeight: 900, color: "rgba(255,255,255,0.92)" }}>
     336                            Token
     337                          </div>
     338                          <div className="tb-mono">
     339                            {token.slice(0, 12)}…{token.slice(-10)}
     340                          </div>
     341                        </div>
     342                        <button className="tb-btn" onClick={copyToken}>
     343                          Copy
     344                        </button>
     345                      </div>
     346
    462347                      <div className="tb-help">
    463                         Овој env ќе се користи за dashboard филтрирање + token generation.
     348                        Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>.
    464349                      </div>
    465                     </div>
    466                   </div>
    467 
    468                   {/* 3) CREATE ENV */}
    469                   <div className="tb-card">
    470                     <div className="tb-card-head">
    471                       <div className="tb-card-title">Create new environment</div>
    472                     </div>
    473 
    474                     <div className="tb-row">
    475                       <input
    476                         className="tb-input"
    477                         value={newEnvName}
    478                         onChange={(e) => setNewEnvName(e.target.value)}
    479                         placeholder="e.g. school-lab / staging"
    480                       />
    481                       <button className="tb-btn" onClick={createEnvironment} disabled={busy}>
    482                         {busy ? "Creating…" : "Create"}
    483                       </button>
    484                     </div>
    485                   </div>
    486 
    487                   {/* 4) TOKEN */}
    488                   <div className="tb-card">
    489                     <div className="tb-card-head">
    490                       <div className="tb-card-title">Step 3: Generate token (for clients)</div>
    491                     </div>
    492 
    493                     <div className="tb-row">
    494                       <button className="tb-btn primary" onClick={generateToken} disabled={busy}>
    495                         {busy ? "Generating…" : `Generate token for "${selectedEnv}"`}
    496                       </button>
    497 
    498                       {token && (
    499                         <>
    500                           <div className="tb-token">
    501                             <div>
    502                               <div style={{ fontWeight: 900, color: "rgba(255,255,255,0.92)" }}>Token</div>
    503                               <div className="tb-mono">
    504                                 {token.slice(0, 12)}…{token.slice(-10)}
    505                               </div>
    506                             </div>
    507                             <button className="tb-btn" onClick={copyToken}>Copy</button>
    508                           </div>
    509 
    510                           <div className="tb-help">
    511                             Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>.
    512                           </div>
    513                         </>
    514                       )}
    515                     </div>
    516                   </div>
    517                 </>
    518               )}
     350                    </>
     351                  )}
     352                </div>
     353              </div>
    519354
    520355              {error && (
  • lan-frontend/src/index.css

    r2058e5c r505f39a  
    15641564}
    15651565
    1566 @tailwind base;
    1567 @tailwind components;
    1568 @tailwind utilities;
     1566.aiDrawer-backdrop{
     1567  position: fixed;
     1568  inset: 0;
     1569  background: rgba(0,0,0,.55);
     1570  backdrop-filter: blur(2px);
     1571  z-index: 999;
     1572}
     1573
     1574/* десен drawer */
     1575.aiDrawer{
     1576  position: fixed;
     1577  top: 0;
     1578  right: 0;
     1579  height: 100vh;
     1580  width: min(520px, 92vw);
     1581  background: #0b1220;            /* темна */
     1582  color: #e7eefc;
     1583  border-left: 1px solid rgba(255,255,255,.08);
     1584  z-index: 1000;
     1585  display: grid;
     1586  grid-template-rows: auto auto 1fr auto;
     1587
     1588  /* slide in */
     1589  transform: translateX(0);
     1590  animation: aiSlideIn .18s ease-out;
     1591}
     1592
     1593@keyframes aiSlideIn{
     1594  from { transform: translateX(24px); opacity: .7; }
     1595  to   { transform: translateX(0); opacity: 1; }
     1596}
     1597
     1598.aiDrawer-head{
     1599  padding: 14px 16px;
     1600  display: flex;
     1601  align-items: center;
     1602  justify-content: space-between;
     1603  border-bottom: 1px solid rgba(255,255,255,.08);
     1604}
     1605
     1606.aiDrawer-title{
     1607  font-size: 16px;
     1608  font-weight: 700;
     1609}
     1610
     1611.aiDrawer-sub{
     1612  font-size: 12px;
     1613  opacity: .75;
     1614  margin-top: 2px;
     1615}
     1616
     1617.aiDrawer-toolbar{
     1618  padding: 12px 16px;
     1619  border-bottom: 1px solid rgba(255,255,255,.08);
     1620  display: grid;
     1621  gap: 8px;
     1622}
     1623
     1624.aiSelect{
     1625  width: 100%;
     1626  background: rgba(255,255,255,.06);
     1627  border: 1px solid rgba(255,255,255,.10);
     1628  color: #e7eefc;
     1629  padding: 10px 12px;
     1630  border-radius: 10px;
     1631  outline: none;
     1632}
     1633
     1634.aiHint{
     1635  font-size: 12px;
     1636  opacity: .75;
     1637}
     1638
     1639.aiDrawer-body{
     1640  padding: 14px 16px;
     1641  overflow: auto;
     1642  display: grid;
     1643  gap: 10px;
     1644}
     1645
     1646/* chat bubbles */
     1647.aiMsg{
     1648  max-width: 92%;
     1649  border: 1px solid rgba(255,255,255,.08);
     1650  border-radius: 14px;
     1651  padding: 10px 12px;
     1652  background: rgba(255,255,255,.04);
     1653}
     1654
     1655.aiMsg.user{
     1656  margin-left: auto;
     1657  background: rgba(88, 167, 255, .12);
     1658  border-color: rgba(88, 167, 255, .20);
     1659}
     1660
     1661.aiMsg-meta{
     1662  font-size: 11px;
     1663  opacity: .7;
     1664  margin-bottom: 6px;
     1665}
     1666
     1667.aiMsg-text{
     1668  white-space: pre-wrap;
     1669  line-height: 1.35;
     1670  font-size: 14px;
     1671}
     1672
     1673.aiDrawer-foot{
     1674  padding: 12px 16px;
     1675  border-top: 1px solid rgba(255,255,255,.08);
     1676  display: grid;
     1677  grid-template-columns: 1fr auto;
     1678  gap: 10px;
     1679  align-items: end;
     1680  background: rgba(255,255,255,.02);
     1681}
     1682
     1683.aiTextarea{
     1684  width: 100%;
     1685  resize: none;
     1686  background: rgba(255,255,255,.06);
     1687  border: 1px solid rgba(255,255,255,.10);
     1688  color: #e7eefc;
     1689  padding: 10px 12px;
     1690  border-radius: 12px;
     1691  outline: none;
     1692}
     1693
     1694.aiBtn{
     1695  background: rgba(255,255,255,.06);
     1696  border: 1px solid rgba(255,255,255,.10);
     1697  color: #e7eefc;
     1698  padding: 10px 12px;
     1699  border-radius: 12px;
     1700  cursor: pointer;
     1701}
     1702
     1703.aiBtn:disabled{
     1704  opacity: .5;
     1705  cursor: not-allowed;
     1706}
     1707
     1708.aiBtnPrimary{
     1709  background: rgba(88, 167, 255, .22);
     1710  border-color: rgba(88, 167, 255, .35);
     1711}
  • lan-frontend/src/main.jsx

    r2058e5c r505f39a  
    1 import React from 'react';                    // ⬅ додадено
    2 import ReactDOM from 'react-dom/client';
    3 import App from './App.jsx';
    4 import './index.css';
     1import React from "react";
     2import ReactDOM from "react-dom/client";
     3import { GoogleOAuthProvider } from "@react-oauth/google";
     4import App from "./App.jsx";
     5import "./styles/theme.css";
    56
    6 ReactDOM.createRoot(document.getElementById('root')).render(
     7ReactDOM.createRoot(document.getElementById("root")).render(
    78  <React.StrictMode>
    8     <App />
     9    <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
     10      <App />
     11    </GoogleOAuthProvider>
    912  </React.StrictMode>
    1013);
  • lan-frontend/src/styles/theme.css

    r2058e5c r505f39a  
    402402}
    403403*::-webkit-scrollbar-track{ background: rgba(2,6,23,0.25); }
     404.aiDrawer-backdrop{
     405  position: fixed;
     406  inset: 0;
     407  background: rgba(0,0,0,.55);
     408  backdrop-filter: blur(2px);
     409  z-index: 999;
     410}
     411
     412/* десен drawer */
     413.aiDrawer{
     414  position: fixed;
     415  top: 0;
     416  right: 0;
     417  height: 100vh;
     418  width: min(520px, 92vw);
     419  background: #0b1220;            /* темна */
     420  color: #e7eefc;
     421  border-left: 1px solid rgba(255,255,255,.08);
     422  z-index: 1000;
     423  display: grid;
     424  grid-template-rows: auto auto 1fr auto;
     425
     426  /* slide in */
     427  transform: translateX(0);
     428  animation: aiSlideIn .18s ease-out;
     429}
     430
     431@keyframes aiSlideIn{
     432  from { transform: translateX(24px); opacity: .7; }
     433  to   { transform: translateX(0); opacity: 1; }
     434}
     435
     436.aiDrawer-head{
     437  padding: 14px 16px;
     438  display: flex;
     439  align-items: center;
     440  justify-content: space-between;
     441  border-bottom: 1px solid rgba(255,255,255,.08);
     442}
     443
     444.aiDrawer-title{
     445  font-size: 16px;
     446  font-weight: 700;
     447}
     448
     449.aiDrawer-sub{
     450  font-size: 12px;
     451  opacity: .75;
     452  margin-top: 2px;
     453}
     454
     455.aiDrawer-toolbar{
     456  padding: 12px 16px;
     457  border-bottom: 1px solid rgba(255,255,255,.08);
     458  display: grid;
     459  gap: 8px;
     460}
     461
     462.aiSelect{
     463  width: 100%;
     464  background: rgba(255,255,255,.06);
     465  border: 1px solid rgba(255,255,255,.10);
     466  color: #e7eefc;
     467  padding: 10px 12px;
     468  border-radius: 10px;
     469  outline: none;
     470}
     471
     472.aiHint{
     473  font-size: 12px;
     474  opacity: .75;
     475}
     476
     477.aiDrawer-body{
     478  padding: 14px 16px;
     479  overflow: auto;
     480  display: grid;
     481  gap: 10px;
     482}
     483
     484/* chat bubbles */
     485.aiMsg{
     486  max-width: 92%;
     487  border: 1px solid rgba(255,255,255,.08);
     488  border-radius: 14px;
     489  padding: 10px 12px;
     490  background: rgba(255,255,255,.04);
     491}
     492
     493.aiMsg.user{
     494  margin-left: auto;
     495  background: rgba(88, 167, 255, .12);
     496  border-color: rgba(88, 167, 255, .20);
     497}
     498
     499.aiMsg-meta{
     500  font-size: 11px;
     501  opacity: .7;
     502  margin-bottom: 6px;
     503}
     504
     505.aiMsg-text{
     506  white-space: pre-wrap;
     507  line-height: 1.35;
     508  font-size: 14px;
     509}
     510
     511.aiDrawer-foot{
     512  padding: 12px 16px;
     513  border-top: 1px solid rgba(255,255,255,.08);
     514  display: grid;
     515  grid-template-columns: 1fr auto;
     516  gap: 10px;
     517  align-items: end;
     518  background: rgba(255,255,255,.02);
     519}
     520
     521.aiTextarea{
     522  width: 100%;
     523  resize: none;
     524  background: rgba(255,255,255,.06);
     525  border: 1px solid rgba(255,255,255,.10);
     526  color: #e7eefc;
     527  padding: 10px 12px;
     528  border-radius: 12px;
     529  outline: none;
     530}
     531
     532.aiBtn{
     533  background: rgba(255,255,255,.06);
     534  border: 1px solid rgba(255,255,255,.10);
     535  color: #e7eefc;
     536  padding: 10px 12px;
     537  border-radius: 12px;
     538  cursor: pointer;
     539}
     540
     541.aiBtn:disabled{
     542  opacity: .5;
     543  cursor: not-allowed;
     544}
     545
     546.aiBtnPrimary{
     547  background: rgba(88, 167, 255, .22);
     548  border-color: rgba(88, 167, 255, .35);
     549}
  • lan-frontend/vite.config.js

    r2058e5c r505f39a  
    1 import { defineConfig } from 'vite'
    2 import react from '@vitejs/plugin-react'
     1// import { defineConfig } from "vite";
     2// import react from "@vitejs/plugin-react";
     3//
     4// export default defineConfig({
     5//   plugins: [react()],
     6//   server: {
     7//     proxy: {
     8//       "/api": { target: "http://localhost:5555", changeOrigin: true },
     9//       "/receive": { target: "http://localhost:5555", changeOrigin: true },
     10//       "/ping": { target: "http://localhost:5555", changeOrigin: true },
     11//     },
     12//   },
     13// });
     14import { defineConfig } from "vite";
     15import react from "@vitejs/plugin-react";
    316
    4 // https://vite.dev/config/
    5 export default {
     17export default defineConfig({
     18  plugins: [react()],
    619  server: {
    720    proxy: {
    8       "/api": "http://localhost:5555",
     21      "/api": {
     22        target: "http://localhost:5555",
     23        changeOrigin: true,
     24        secure: false,
     25      },
     26      "/receive": {
     27        target: "http://localhost:5555",
     28        changeOrigin: true,
     29        secure: false,
     30      },
    931    },
    1032  },
    11 };
     33});
Note: See TracChangeset for help on using the changeset viewer.