Changeset 505f39a for lan-frontend/src/components
- Timestamp:
- 01/21/26 13:35:44 (6 months ago)
- Branches:
- master
- Children:
- d42aac3
- Parents:
- 2058e5c
- Location:
- lan-frontend/src/components
- Files:
-
- 1 added
- 3 edited
-
AIAssistantDrawer.jsx (modified) (2 diffs)
-
ComputerDetailsModal.jsx (modified) (7 diffs)
-
Login.jsx (added)
-
TopBar.jsx (modified) (12 diffs)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/components/AIAssistantDrawer.jsx
r2058e5c r505f39a 1 import React from "react";1 import React, {useEffect, useRef, useState} from "react"; 2 2 3 import { useEffect, useRef, useState } from "react"; 3 export 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); 4 20 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]); 12 31 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]); 16 35 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; 20 39 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); 24 43 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 } 37 77 } 38 }39 78 40 if (!open) return null;79 if (!open) return null; 41 80 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}/> 53 84 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> 65 96 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> 77 101 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" 82 137 rows={2} 83 138 value={input} … … 85 140 onChange={(e) => setInput(e.target.value)} 86 141 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 } 91 146 }} 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 ); 101 159 } -
lan-frontend/src/components/ComputerDetailsModal.jsx
r2058e5c r505f39a 2 2 3 3 function severityForEventId(eventId) { 4 const id = parseInt(eventId, 10);4 const id = Number.parseInt(String(eventId ?? ""), 10); 5 5 const high = new Set([1, 3, 8, 10]); 6 6 const med = new Set([5, 6, 7, 11, 22]); … … 10 10 } 11 11 12 function fmtTime(ts) { 13 if (!ts) return "—"; 14 try { 15 return new Date(ts).toLocaleTimeString(); 16 } catch { 17 return String(ts); 18 } 19 } 20 12 21 export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) { 13 22 const [tab, setTab] = useState("overview"); 14 23 const [loading, setLoading] = useState(false); 15 24 const [details, setDetails] = useState(null); 25 const [error, setError] = useState(""); 16 26 const [search, setSearch] = useState(""); 17 27 18 // JSON viewer state28 // JSON viewer 19 29 const [jsonOpen, setJsonOpen] = useState(false); 20 30 const [jsonTitle, setJsonTitle] = useState(""); 21 31 const [jsonData, setJsonData] = useState(null); 22 32 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 23 56 useEffect(() => { 24 57 if (!open || !computer?.name) return; … … 26 59 27 60 async function load() { 61 setLoading(true); 62 setError(""); 28 63 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}`); 32 69 if (!cancelled) setDetails(d); 33 70 } catch (e) { 34 console.error("details error", e); 71 if (!cancelled) { 72 setError(String(e?.message || e)); 73 setDetails(null); 74 } 35 75 } finally { 36 76 if (!cancelled) setLoading(false); … … 51 91 const msg = (ev.message || "").toLowerCase(); 52 92 const type = (ev.event_type || "").toLowerCase(); 53 const id = String(ev.event_id ?? "") ;93 const id = String(ev.event_id ?? "").toLowerCase(); 54 94 return msg.includes(q) || type.includes(q) || id.includes(q); 55 95 }); … … 57 97 58 98 function openJson(ev) { 59 // server ти враќа: { event_id, event_type, message, timestamp, details }60 99 setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`); 61 setJsonData(ev?.details ?? ev); // ако нема details, покажи цел event100 setJsonData(ev?.details ?? ev); 62 101 setJsonOpen(true); 63 102 } … … 73 112 return ( 74 113 <> 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"> 78 312 <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 || "—"} 82 316 </div> 83 317 </div> 84 318 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")}> 88 326 Overview 89 327 </button> 90 <button className={` tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>328 <button className={`cdm-tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}> 91 329 Sysmon 92 330 </button> 93 <button className={` tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>331 <button className={`cdm-tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}> 94 332 Processes 95 333 </button> 96 <button className={` tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>334 <button className={`cdm-tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}> 97 335 Network 98 336 </button> 99 337 </div> 100 <button className="btn" onClick={onClose}> 338 339 <button className="cdm-btn" onClick={onClose}> 101 340 Close 102 341 </button> … … 104 343 </div> 105 344 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> 116 357 </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> 135 373 </div> 136 374 </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 )} 153 443 </div> 154 444 </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> 195 467 </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> 213 493 </div> 214 )}494 </div> 215 495 </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> 223 546 </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> 333 553 </div> 334 554 </div> 335 555 </div> 336 556 337 {/* JSON VIEWER MODAL*/}557 {/* JSON VIEWER */} 338 558 {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"> 342 562 <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> 345 565 </div> 346 <div style={{ display: "flex", gap: 10 }}>566 <div className="cdm-right"> 347 567 <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))} 353 570 > 354 571 Copy 355 572 </button> 356 <button className=" btn" onClick={closeJson}>573 <button className="cdm-btn" onClick={closeJson}> 357 574 Close 358 575 </button> 359 576 </div> 360 577 </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> 378 581 </div> 379 582 </div> -
lan-frontend/src/components/TopBar.jsx
r2058e5c r505f39a 1 1 import React, { useEffect, useMemo, useState } from "react"; 2 3 export default function TopBar({ onOpenAI, onRefresh, lastUpdated, onEnvChange }) { 2 import logo from "../assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png"; 3 4 export 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 }) { 4 13 const [open, setOpen] = useState(false); 5 6 // admin auth7 const [adminKey, setAdminKey] = useState("");8 const [adminSession, setAdminSession] = useState("");9 14 10 15 // env + tokens 11 16 const [envs, setEnvs] = useState(["default"]); 12 const [selectedEnv, setSelectedEnv] = useState( "default");17 const [selectedEnv, setSelectedEnv] = useState(selectedEnvProp || "default"); 13 18 const [newEnvName, setNewEnvName] = useState(""); 14 19 const [token, setToken] = useState(""); … … 17 22 const [error, setError] = useState(""); 18 23 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() { 21 34 const r = await fetch("/api/admin/environments", { 22 headers: { "X-Admin-Session": session },35 credentials: "include", 23 36 }); 24 37 const data = await r.json(); … … 28 41 setEnvs(list); 29 42 30 // ако тековниот не постои, стави првиот од листата31 43 if (!list.includes(selectedEnv)) { 32 44 setSelectedEnv(list[0]); … … 35 47 } 36 48 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 76 49 async function createEnvironment() { 77 50 setError(""); 78 51 setToken(""); 52 79 53 if (!newEnvName.trim()) { 80 54 setError("Внеси име за environment."); … … 86 60 const r = await fetch("/api/admin/environments", { 87 61 method: "POST", 88 headers: { 89 "Content-Type": "application/json", 90 "X-Admin-Session": adminSession, 91 }, 62 headers: { "Content-Type": "application/json" }, 63 credentials: "include", 92 64 body: JSON.stringify({ name: newEnvName.trim() }), 93 65 }); … … 96 68 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`); 97 69 98 // reload list, select new env 99 await loadEnvironments(adminSession); 70 await loadEnvironments(); 100 71 setSelectedEnv(newEnvName.trim()); 101 72 onEnvChange?.(newEnvName.trim()); … … 120 91 const r = await fetch("/api/admin/tokens", { 121 92 method: "POST", 122 headers: { 123 "Content-Type": "application/json", 124 "X-Admin-Session": adminSession, 125 }, 93 headers: { "Content-Type": "application/json" }, 94 credentials: "include", 126 95 body: JSON.stringify({ env: selectedEnv }), 127 96 }); … … 147 116 setToken(""); 148 117 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(""); 149 129 } 150 130 … … 161 141 <> 162 142 <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); 169 144 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; } 358 191 .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); } 384 200 `}</style> 385 201 386 {/* TOPBAR */}387 202 <div className="tb-wrap"> 388 203 <div className="tb-inner"> … … 394 209 Sysmon Edition • <span>{prettyTime}</span> 395 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 )} 396 217 </div> 397 218 </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 398 230 399 231 <div className="tb-actions"> … … 402 234 Env: {selectedEnv} 403 235 </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> 407 257 </div> 408 258 </div> 409 259 </div> 410 260 411 {/* MODAL */}412 261 {open && ( 413 262 <div className="tb-backdrop" onMouseDown={() => setOpen(false)}> … … 415 264 <div className="tb-modal-head"> 416 265 <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> 421 279 </div> 422 280 423 281 <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. 429 300 </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 462 347 <div className="tb-help"> 463 Овој env ќе се користи за dashboard филтрирање + token generation.348 Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>. 464 349 </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> 519 354 520 355 {error && (
Note:
See TracChangeset
for help on using the changeset viewer.
