Changeset d42aac3 for lan-frontend/src


Ignore:
Timestamp:
01/22/26 17:32:29 (6 months ago)
Author:
istevanoska <ilinastevanoska@…>
Branches:
master
Children:
e4c61dd
Parents:
505f39a
Message:

Added history mmanage

Location:
lan-frontend/src
Files:
1 deleted
3 edited
1 moved

Legend:

Unmodified
Added
Removed
  • lan-frontend/src/App.jsx

    r505f39a rd42aac3  
    1 import React, {useEffect, useMemo, useState} from "react";
     1import React, { useEffect, useMemo, useState } from "react";
    22import "./styles/theme.css";
    33
     
    99
    1010export default function App() {
    11     const [stats, setStats] = useState(null);
    12     const [computers, setComputers] = useState([]);
    13     const [loading, setLoading] = useState(true);
    14     const [lastUpdated, setLastUpdated] = useState(null);
    15 
    16     const [query, setQuery] = useState("");
    17     const [status, setStatus] = useState("");
    18 
    19     const [selectedComputer, setSelectedComputer] = useState(null);
    20     const [aiOpen, setAiOpen] = useState(false);
    21     const [aiScope, setAiScope] = useState(null);
    22     const [selectedEnv, setSelectedEnv] = useState("default");
    23 
    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 
    49     async function refresh() {
    50         try {
    51             setLoading(true);
    52             const [s, c] = await Promise.all([
    53                 fetch("/api/stats", {credentials: "include"}).then((r) => r.json()),
    54                 fetch("/api/computers", {
    55                     credentials: "include",
    56                     headers: {"X-Env": selectedEnv},
    57                 }).then((r) => r.json()),
    58             ]);
    59 
    60             setStats(s);
    61             setComputers(Array.isArray(c) ? c : []);
    62             setLastUpdated(new Date().toISOString());
    63         } catch (e) {
    64             console.error("refresh error", e);
    65         } finally {
    66             setLoading(false);
    67         }
    68     }
    69 
    70     // on mount: check session
    71     useEffect(() => {
    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]);
    80 
    81     const filtered = useMemo(() => {
    82         const q = query.trim().toLowerCase();
    83         return (computers || [])
    84             .filter((c) => (status ? c.status === status : true))
    85             .filter((c) => {
    86                 if (!q) return true;
    87                 const hay = [c.name, c.user, c.ip, c.os, c.status]
    88                     .filter(Boolean)
    89                     .join(" ")
    90                     .toLowerCase();
    91                 return hay.includes(q);
    92             });
    93     }, [computers, query, status]);
    94 
    95     // Gate UI
    96     if (meLoading) return <div style={{padding: 20}}>Loading…</div>;
    97     if (!me) return <Login onDone={loadMe}/>;
    98 
    99     return (
    100         <div className="app-wrap">
    101             <TopBar
    102                 onOpenAI={() => setAiOpen(true)}
    103                 onRefresh={refresh}
    104                 lastUpdated={lastUpdated}
    105                 onEnvChange={(env) => setSelectedEnv(env)}
    106                 selectedEnv={selectedEnv}
    107                 me={me}
    108                 onLoggedOut={loadMe}
     11  const [stats, setStats] = useState(null);
     12  const [computers, setComputers] = useState([]);
     13  const [loading, setLoading] = useState(true);
     14  const [lastUpdated, setLastUpdated] = useState(null);
     15
     16  const [query, setQuery] = useState("");
     17  const [status, setStatus] = useState("");
     18
     19  const [selectedComputer, setSelectedComputer] = useState(null);
     20  const [aiOpen, setAiOpen] = useState(false);
     21  const [aiScope, setAiScope] = useState(null);
     22  const [selectedEnv, setSelectedEnv] = useState("default");
     23
     24  const [me, setMe] = useState(null);
     25  const [meLoading, setMeLoading] = useState(true);
     26
     27  // NEW: environment setting (switch)
     28  const [saveProcHistory, setSaveProcHistory] = useState(false);
     29
     30  async function loadMe() {
     31    setMeLoading(true);
     32    try {
     33      const r = await fetch("/api/me", { credentials: "include" });
     34      if (r.ok) {
     35        const data = await r.json();
     36        setMe(data.user);
     37      } else {
     38        setMe(null);
     39      }
     40    } catch (e) {
     41      console.error("loadMe error", e);
     42      setMe(null);
     43    } finally {
     44      setMeLoading(false);
     45    }
     46  }
     47
     48  async function refresh() {
     49    try {
     50      setLoading(true);
     51      const [s, c] = await Promise.all([
     52        fetch("/api/stats", { credentials: "include" }).then((r) => r.json()),
     53        fetch("/api/computers", {
     54          credentials: "include",
     55          headers: { "X-Env": selectedEnv },
     56        }).then((r) => r.json()),
     57      ]);
     58
     59      setStats(s);
     60      setComputers(Array.isArray(c) ? c : []);
     61      setLastUpdated(new Date().toISOString());
     62    } catch (e) {
     63      console.error("refresh error", e);
     64    } finally {
     65      setLoading(false);
     66    }
     67  }
     68
     69  // NEW: load env settings (switch state) for selected env
     70  async function loadEnvSettings(env) {
     71    try {
     72      // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false)
     73      const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
     74        credentials: "include",
     75      });
     76
     77      if (r.ok) {
     78        const data = await r.json();
     79        setSaveProcHistory(!!data.save_process_history);
     80      } else {
     81        setSaveProcHistory(false);
     82      }
     83    } catch (e) {
     84      console.error("loadEnvSettings error", e);
     85      setSaveProcHistory(false);
     86    }
     87  }
     88
     89  // NEW: set env setting (toggle)
     90  async function setEnvProcessHistory(env, enabled) {
     91    // optimistic UI
     92    setSaveProcHistory(enabled);
     93
     94    try {
     95      const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {
     96        method: "POST",
     97        credentials: "include",
     98        headers: { "Content-Type": "application/json" },
     99        body: JSON.stringify({ save_process_history: enabled }),
     100      });
     101
     102      if (r.ok) {
     103        const data = await r.json();
     104        setSaveProcHistory(!!data.save_process_history);
     105      } else {
     106        // rollback
     107        setSaveProcHistory(!enabled);
     108        console.error("setEnvProcessHistory failed", await r.text());
     109      }
     110    } catch (e) {
     111      setSaveProcHistory(!enabled);
     112      console.error("setEnvProcessHistory error", e);
     113    }
     114  }
     115
     116  // on mount: check session
     117  useEffect(() => {
     118    loadMe();
     119  }, []);
     120
     121  // when logged in OR env changed, refresh data + load env switch state
     122  useEffect(() => {
     123    if (!me) return;
     124    loadEnvSettings(selectedEnv);
     125    refresh();
     126    // eslint-disable-next-line react-hooks/exhaustive-deps
     127  }, [me, selectedEnv]);
     128
     129  const filtered = useMemo(() => {
     130    const q = query.trim().toLowerCase();
     131    return (computers || [])
     132      .filter((c) => (status ? c.status === status : true))
     133      .filter((c) => {
     134        if (!q) return true;
     135        const hay = [c.name, c.user, c.ip, c.os, c.status]
     136          .filter(Boolean)
     137          .join(" ")
     138          .toLowerCase();
     139        return hay.includes(q);
     140      });
     141  }, [computers, query, status]);
     142
     143  // Gate UI
     144  if (meLoading) return <div style={{ padding: 20 }}>Loading…</div>;
     145  if (!me) return <Login onDone={loadMe} />;
     146
     147  return (
     148    <div className="app-wrap">
     149      <TopBar
     150        onOpenAI={() => setAiOpen(true)}
     151        onRefresh={refresh}
     152        lastUpdated={lastUpdated}
     153        onEnvChange={(env) => setSelectedEnv(env)}
     154        selectedEnv={selectedEnv}
     155        me={me}
     156        onLoggedOut={loadMe}
     157        // NEW props for switch:
     158        saveProcHistory={saveProcHistory}
     159        onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)}
     160      />
     161
     162      {/* STATS */}
     163      <div className="stats-grid">
     164        <div className="stat-card">
     165          <div className="label">Total Computers</div>
     166          <div className="value">{computers.length}</div>
     167          <div className="sub">Monitored Systems</div>
     168        </div>
     169
     170        <div className="stat-card">
     171          <div className="label">Sysmon Events (24h)</div>
     172          <div className="value">{stats?.total_sysmon_events ?? "—"}</div>
     173          <div className="sub">Security Events</div>
     174        </div>
     175
     176        <div className="stat-card">
     177          <div className="label">Network Connections (24h)</div>
     178          <div className="value">{stats?.total_network_connections ?? "—"}</div>
     179          <div className="sub">Captured Connections</div>
     180        </div>
     181      </div>
     182
     183      {/* FILTERS */}
     184      <div className="panel" style={{ marginBottom: 14 }}>
     185        <div className="panel-head">
     186          <div className="panel-title">Filters</div>
     187          <div style={{ color: "var(--dim)", fontSize: 12 }}>
     188            {loading ? "Refreshing…" : `${filtered.length} shown`}
     189          </div>
     190        </div>
     191        <div className="panel-body">
     192          <div className="filters">
     193            <input
     194              className="input"
     195              value={query}
     196              onChange={(e) => setQuery(e.target.value)}
     197              placeholder="Search by name / user / ip / os…"
    109198            />
    110199
    111 
    112             {/* STATS */}
    113             <div className="stats-grid">
    114                 <div className="stat-card">
    115                     <div className="label">Total Computers</div>
    116                     <div className="value">{computers.length}</div>
    117                     <div className="sub">Monitored Systems</div>
    118                 </div>
    119 
    120                 <div className="stat-card">
    121                     <div className="label">Sysmon Events (24h)</div>
    122                     <div className="value">{stats?.total_sysmon_events ?? "—"}</div>
    123                     <div className="sub">Security Events</div>
    124                 </div>
    125 
    126                 <div className="stat-card">
    127                     <div className="label">Network Connections (24h)</div>
    128                     <div className="value">{stats?.total_network_connections ?? "—"}</div>
    129                     <div className="sub">Captured Connections</div>
    130                 </div>
    131             </div>
    132 
    133             {/* FILTERS */}
    134             <div className="panel" style={{marginBottom: 14}}>
    135                 <div className="panel-head">
    136                     <div className="panel-title">Filters</div>
    137                     <div style={{color: "var(--dim)", fontSize: 12}}>
    138                         {loading ? "Refreshing…" : `${filtered.length} shown`}
    139                     </div>
    140                 </div>
    141                 <div className="panel-body">
    142                     <div className="filters">
    143                         <input
    144                             className="input"
    145                             value={query}
    146                             onChange={(e) => setQuery(e.target.value)}
    147                             placeholder="Search by name / user / ip / os…"
    148                         />
    149 
    150                         <select
    151                             className="select"
    152                             value={status}
    153                             onChange={(e) => setStatus(e.target.value)}
    154                         >
    155                             <option value="">All status</option>
    156                             <option value="online">Online</option>
    157                             <option value="idle">Idle</option>
    158                             <option value="offline">Offline</option>
    159                         </select>
    160 
    161                         <button className="btn primary" onClick={refresh}>
    162                             Refresh
    163                         </button>
    164                     </div>
    165                 </div>
    166             </div>
    167 
    168             {/* COMPUTERS */}
    169             <div className="panel">
    170                 <div className="panel-head">
    171                     <div className="panel-title">Connected Computers</div>
    172                     <div style={{display: "flex", gap: 10, alignItems: "center"}}>
    173                         <span className="pill">{filtered.length} computers</span>
    174                         <button
    175                             className="btn"
    176                             onClick={() => {
    177                                 setQuery("");
    178                                 setStatus("");
    179                             }}
    180                         >
    181                             Reset
    182                         </button>
    183                     </div>
    184                 </div>
    185                 <div className="panel-body">
    186                     <div className="computers-grid">
    187                         {filtered.map((c) => (
    188                             <ComputerCard
    189                                 key={c.name}
    190                                 c={c}
    191                                 onOpen={(pc) => {
    192                                     setSelectedComputer(pc);
    193                                     setAiScope(pc.name);
    194                                 }}
    195                             />
    196                         ))}
    197                         {filtered.length === 0 && (
    198                             <div style={{color: "var(--dim)"}}>
    199                                 No computers match filter.
    200                             </div>
    201                         )}
    202                     </div>
    203                 </div>
    204             </div>
    205 
    206             {/* DETAILS MODAL */}
    207             <ComputerDetailsModal
    208                 open={!!selectedComputer}
    209                 computer={selectedComputer}
    210                 onClose={() => setSelectedComputer(null)}
    211                 onAskAI={(name) => {
    212                     setAiScope(name);
    213                     setAiOpen(true);
     200            <select
     201              className="select"
     202              value={status}
     203              onChange={(e) => setStatus(e.target.value)}
     204            >
     205              <option value="">All status</option>
     206              <option value="online">Online</option>
     207              <option value="idle">Idle</option>
     208              <option value="offline">Offline</option>
     209            </select>
     210
     211            <button className="btn primary" onClick={refresh}>
     212              Refresh
     213            </button>
     214          </div>
     215        </div>
     216      </div>
     217
     218      {/* COMPUTERS */}
     219      <div className="panel">
     220        <div className="panel-head">
     221          <div className="panel-title">Connected Computers</div>
     222          <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
     223            <span className="pill">{filtered.length} computers</span>
     224            <button
     225              className="btn"
     226              onClick={() => {
     227                setQuery("");
     228                setStatus("");
     229              }}
     230            >
     231              Reset
     232            </button>
     233          </div>
     234        </div>
     235        <div className="panel-body">
     236          <div className="computers-grid">
     237            {filtered.map((c) => (
     238              <ComputerCard
     239                key={c.name}
     240                c={c}
     241                onOpen={(pc) => {
     242                  setSelectedComputer(pc);
     243                  setAiScope(pc.name);
    214244                }}
    215             />
    216 
    217             {/* 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 
    229             <AIAssistantDrawer
    230                 open={aiOpen}
    231                 onClose={() => setAiOpen(false)}
    232                 computers={computers}
    233                 scope={aiScope}
    234                 setScope={setAiScope}
    235                 apiBase={import.meta.env.VITE_API_BASE}
    236             />
    237 
    238 
    239         </div>
    240     );
     245              />
     246            ))}
     247            {filtered.length === 0 && (
     248              <div style={{ color: "var(--dim)" }}>No computers match filter.</div>
     249            )}
     250          </div>
     251        </div>
     252      </div>
     253
     254      {/* DETAILS MODAL */}
     255      <ComputerDetailsModal
     256        open={!!selectedComputer}
     257        computer={selectedComputer}
     258        onClose={() => setSelectedComputer(null)}
     259        onAskAI={(name) => {
     260          setAiScope(name);
     261          setAiOpen(true);
     262        }}
     263      />
     264
     265      {/* AI DRAWER */}
     266      <AIAssistantDrawer
     267        open={aiOpen}
     268        onClose={() => setAiOpen(false)}
     269        computers={computers}
     270        scope={aiScope}
     271        setScope={setAiScope}
     272        apiBase={import.meta.env.VITE_API_BASE}
     273      />
     274    </div>
     275  );
    241276}
  • lan-frontend/src/components/AIAssistantDrawer.jsx

    r505f39a rd42aac3  
    5555
    5656            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            // ]);
    5769            if (!r.ok) {
    58                 setMessages((m) => [...m, {
    59                     role: "assistant",
    60                     text: `Error ${r.status}: ${data.error || "Request failed"}`
    61                 }]);
     70                setMessages((m) => [
     71                    ...m,
     72                    {role: "assistant", text: `Error ${r.status}: ${data.error || "Request failed"}`},
     73                ]);
    6274            } else {
    63                 setMessages((m) => [...m, {role: "assistant", text: data.answer || "(no answer)"}]);
     75                setMessages((m) => [
     76                    ...m,
     77                    {role: "assistant", text: data.answer || "(no answer)"},
     78                ]);
    6479            }
    65             setMessages((m) => [
    66                 ...m,
    67                 {role: "assistant", text: data.answer || "(no answer)"},
    68             ]);
     80
    6981        } catch (e) {
    7082            setMessages((m) => [
  • lan-frontend/src/components/TopBar.jsx

    r505f39a rd42aac3  
    11import React, { useEffect, useMemo, useState } from "react";
    2 import logo from "../assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png";
     2import logo from "../assets/logo_image.png";
    33
    44export default function TopBar({
     
    1010  me,
    1111  onLoggedOut, // optional callback (App loadMe)
     12
     13  // NEW: process history switch (controlled by App)
     14  saveProcHistory = false,
     15  onToggleProcHistory, // (enabled:boolean) => void
    1216}) {
    1317  const [open, setOpen] = useState(false);
     
    2226  const [error, setError] = useState("");
    2327
     28  const isAdmin = (me?.role || "").toLowerCase() === "admin";
    2429
    2530  // keep in sync ако App праќа selectedEnv prop
     
    146151        .tb-inner{ max-width: 1200px; margin: 0 auto; padding: 14px 18px;
    147152          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; }
    156153        .tb-actions{ display:flex; align-items:center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
    157154        .tb-pill{ display:inline-flex; align-items:center; gap: 8px; padding: 8px 12px; border-radius: 999px;
     
    169166          border-color: rgba(147,197,253,0.35); color: rgba(5,12,24,0.95); }
    170167        .tb-btn.primary:hover{ background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75)); }
     168
     169        .tb-switch{ display:flex; align-items:center; gap:10px; padding: 8px 12px; border-radius: 999px;
     170          background: rgba(15,23,42,0.55); border: 1px solid rgba(96,165,250,0.22);
     171          color: rgba(255,255,255,0.9); font-weight: 800; font-size: 12px; }
     172        .tb-switch .label{ color: rgba(191,219,254,0.9); }
     173        .tb-toggle{ width: 46px; height: 26px; border-radius: 999px; position: relative; border: 1px solid rgba(96,165,250,0.25);
     174          background: rgba(30,41,59,0.65); cursor: pointer; transition: background .12s ease, opacity .12s ease; }
     175        .tb-toggle.disabled{ opacity: .55; cursor: not-allowed; }
     176        .tb-knob{ width: 22px; height: 22px; border-radius: 999px; position: absolute; top: 1px; left: 1px;
     177          background: rgba(226,232,240,0.95); transition: transform .14s ease; box-shadow: 0 6px 14px rgba(0,0,0,0.22); }
     178        .tb-toggle.on{ background: rgba(37,99,235,0.85); border-color: rgba(147,197,253,0.45); }
     179        .tb-toggle.on .tb-knob{ transform: translateX(20px); }
    171180
    172181        .tb-backdrop{ position: fixed; inset: 0; background: rgba(2,6,23,0.72); backdrop-filter: blur(6px);
     
    202211      <div className="tb-wrap">
    203212        <div className="tb-inner">
    204           <div className="tb-brand">
    205             <div className="tb-badge">🛡️</div>
    206             <div>
    207               <h1 className="tb-title">LAN Security Monitor</h1>
    208               <p className="tb-sub">
    209                 Sysmon Edition • <span>{prettyTime}</span>
    210               </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               )}
     213          <div>
     214            <img
     215              src={logo}
     216              alt="NETIntel logo"
     217              className="netintel-logo"
     218              width={150}
     219              style={{ marginTop: 10 }}
     220            />
     221            <div style={{ marginTop: 4, fontSize: 12, color: "rgba(191,219,254,0.8)", fontWeight: 800 }}>
     222              <span style={{ color: "rgba(96,165,250,0.95)" }}>{prettyTime}</span>
     223              {me?.email ? (
     224                <span style={{ marginLeft: 10 }}>
     225                  • {me.email}
     226                  {me?.role ? ` • ${me.role}` : ""}
     227                </span>
     228              ) : null}
    217229            </div>
    218230          </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 
    230231
    231232          <div className="tb-actions">
    232             <span className="tb-pill">
     233            <span className="tb-pill" title="Selected environment">
    233234              <span className="dot" />
    234235              Env: {selectedEnv}
    235236            </span>
     237
     238            {/* NEW: Save process history switch */}
     239            <div className="tb-switch" title={isAdmin ? "Toggle saving process history for this environment" : "Only admin can change this"}>
     240              <span className="label">Proc history</span>
     241              <div
     242                className={[
     243                  "tb-toggle",
     244                  saveProcHistory ? "on" : "",
     245                  !isAdmin ? "disabled" : "",
     246                ].join(" ")}
     247                role="switch"
     248                aria-checked={saveProcHistory ? "true" : "false"}
     249                tabIndex={0}
     250                onClick={() => {
     251                  if (!isAdmin) return;
     252                  onToggleProcHistory?.(!saveProcHistory);
     253                }}
     254                onKeyDown={(e) => {
     255                  if (!isAdmin) return;
     256                  if (e.key === "Enter" || e.key === " ") {
     257                    e.preventDefault();
     258                    onToggleProcHistory?.(!saveProcHistory);
     259                  }
     260                }}
     261              >
     262                <div className="tb-knob" />
     263              </div>
     264            </div>
     265
    236266            <button className="tb-btn" onClick={onRefresh} disabled={busy}>
    237267              Refresh
    238268            </button>
     269
    239270            <button
    240271              className="tb-btn"
     
    252283              Admin
    253284            </button>
     285
    254286            <button className="tb-btn primary" onClick={onOpenAI}>
    255287              AI Assistant
     
    298330                  <div className="tb-help">
    299331                    Овој env ќе се користи за dashboard филтрирање + token generation.
     332                    <br />
     333                    <b>Proc history</b> switch горе важи за тековниот env.
    300334                  </div>
    301335                </div>
Note: See TracChangeset for help on using the changeset viewer.