Changeset d42aac3 for lan-frontend
- Timestamp:
- 01/22/26 17:32:29 (6 months ago)
- Branches:
- master
- Children:
- e4c61dd
- Parents:
- 505f39a
- Location:
- lan-frontend/src
- Files:
-
- 1 deleted
- 3 edited
- 1 moved
-
App.jsx (modified) (2 diffs)
-
assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview.png (deleted)
-
assets/logo_image.png (moved) (moved from lan-frontend/src/assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png )
-
components/AIAssistantDrawer.jsx (modified) (1 diff)
-
components/TopBar.jsx (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/App.jsx
r505f39a rd42aac3 1 import React, { useEffect, useMemo, useState} from "react";1 import React, { useEffect, useMemo, useState } from "react"; 2 2 import "./styles/theme.css"; 3 3 … … 9 9 10 10 export 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…" 109 198 /> 110 199 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); 214 244 }} 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 ); 241 276 } -
lan-frontend/src/components/AIAssistantDrawer.jsx
r505f39a rd42aac3 55 55 56 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 // ]); 57 69 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 ]); 62 74 } 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 ]); 64 79 } 65 setMessages((m) => [ 66 ...m, 67 {role: "assistant", text: data.answer || "(no answer)"}, 68 ]); 80 69 81 } catch (e) { 70 82 setMessages((m) => [ -
lan-frontend/src/components/TopBar.jsx
r505f39a rd42aac3 1 1 import React, { useEffect, useMemo, useState } from "react"; 2 import logo from "../assets/ ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png";2 import logo from "../assets/logo_image.png"; 3 3 4 4 export default function TopBar({ … … 10 10 me, 11 11 onLoggedOut, // optional callback (App loadMe) 12 13 // NEW: process history switch (controlled by App) 14 saveProcHistory = false, 15 onToggleProcHistory, // (enabled:boolean) => void 12 16 }) { 13 17 const [open, setOpen] = useState(false); … … 22 26 const [error, setError] = useState(""); 23 27 28 const isAdmin = (me?.role || "").toLowerCase() === "admin"; 24 29 25 30 // keep in sync ако App праќа selectedEnv prop … … 146 151 .tb-inner{ max-width: 1200px; margin: 0 auto; padding: 14px 18px; 147 152 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 153 .tb-actions{ display:flex; align-items:center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; } 157 154 .tb-pill{ display:inline-flex; align-items:center; gap: 8px; padding: 8px 12px; border-radius: 999px; … … 169 166 border-color: rgba(147,197,253,0.35); color: rgba(5,12,24,0.95); } 170 167 .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); } 171 180 172 181 .tb-backdrop{ position: fixed; inset: 0; background: rgba(2,6,23,0.72); backdrop-filter: blur(6px); … … 202 211 <div className="tb-wrap"> 203 212 <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} 217 229 </div> 218 230 </div> 219 <div>220 <img221 src={logo}222 alt="NETIntel logo"223 className="netintel-logo"224 width={150}225 style={{marginTop: 10 }}226 />227 </div>228 229 230 231 231 232 <div className="tb-actions"> 232 <span className="tb-pill" >233 <span className="tb-pill" title="Selected environment"> 233 234 <span className="dot" /> 234 235 Env: {selectedEnv} 235 236 </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 236 266 <button className="tb-btn" onClick={onRefresh} disabled={busy}> 237 267 Refresh 238 268 </button> 269 239 270 <button 240 271 className="tb-btn" … … 252 283 Admin 253 284 </button> 285 254 286 <button className="tb-btn primary" onClick={onOpenAI}> 255 287 AI Assistant … … 298 330 <div className="tb-help"> 299 331 Овој env ќе се користи за dashboard филтрирање + token generation. 332 <br /> 333 <b>Proc history</b> switch горе важи за тековниот env. 300 334 </div> 301 335 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
