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

Added OAuth/prototype

File:
1 edited

Legend:

Unmodified
Added
Removed
  • lan-frontend/src/components/ComputerDetailsModal.jsx

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