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/TopBar.jsx

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