mirror of
https://github.com/SquidSec/SquidC5
synced 2026-08-09 12:22:26 +00:00
Merge pull request #70 from SquidSec/fix/ops-ux-nav-ai-brand-scroll
fix(ops): INK brand, mobile drawer nav, resizable dock
This commit is contained in:
@@ -266,7 +266,8 @@ OPENAI_TOOLS: list[dict[str, Any]] = [
|
||||
},
|
||||
]
|
||||
|
||||
CHAT_SYSTEM_PROMPT = """You are SquidC5 Admin AI — the operator assistant for this C5 (Command, Control, Cognitive, Collaborative, Coordination) teamserver.
|
||||
CHAT_SYSTEM_PROMPT = """You are INK — SquidC5's neural operator assistant for this C5 (Command, Control, Cognitive, Collaborative, Coordination) teamserver.
|
||||
When referring to yourself, use the name INK.
|
||||
|
||||
You can:
|
||||
1) Answer general security/ops questions clearly and helpfully.
|
||||
|
||||
+46
-61
@@ -69,7 +69,7 @@
|
||||
{ a: "hitl", t: "HITL queue" },
|
||||
],
|
||||
ai: [
|
||||
{ a: "admin-ai", t: "Admin AI" },
|
||||
{ a: "admin-ai", t: "INK" },
|
||||
{ a: "llm-connections", t: "LLM connections" },
|
||||
],
|
||||
observe: [
|
||||
@@ -1095,46 +1095,36 @@
|
||||
const root = el("view-ai");
|
||||
if (!root) return;
|
||||
if (!can("ai:use") && !can("admin")) {
|
||||
root.innerHTML = `<div class="empty-state"><strong>AI locked</strong>Need <code>ai:use</code> scope.</div>`;
|
||||
root.innerHTML = `<div class="empty-state"><strong>INK locked</strong>Need <code>ai:use</code> scope.</div>`;
|
||||
return;
|
||||
}
|
||||
root.innerHTML = `
|
||||
<div class="form-grid ai-layout">
|
||||
<div class="work-panel">
|
||||
<div class="wp-head">Operator chat</div>
|
||||
<div class="wp-body">
|
||||
<p class="muted" style="font-size:0.78rem;margin:0 0 8px">
|
||||
Free-form chat with railed tools: sessions, listeners, tasks, payloads, metrics, events, audit.
|
||||
Example: <em>“Setup a reverse shell listener on 4444 and start it.”</em>
|
||||
</p>
|
||||
<label>LLM connection</label>
|
||||
<select id="aiLlmPick"><option value="">Loading…</option></select>
|
||||
<label>Message</label>
|
||||
<textarea id="aiData" rows="4" placeholder="Ask a question or give an ops instruction…"></textarea>
|
||||
<div class="row">
|
||||
<button type="button" class="primary" id="aiRun">Send</button>
|
||||
<button type="button" id="aiClearPage">Clear chat</button>
|
||||
<button type="button" id="aiStatus">AI status</button>
|
||||
<button type="button" id="aiTools">Tools</button>
|
||||
<button type="button" id="aiOpenDrawer">Floating chat</button>
|
||||
</div>
|
||||
<div id="aiPageLog" class="outbox" style="max-height:min(420px,45vh);min-height:160px"></div>
|
||||
<details style="margin-top:10px">
|
||||
<summary class="muted" style="cursor:pointer;font-size:0.78rem">Legacy capability runner</summary>
|
||||
<label style="margin-top:8px">Capability</label>
|
||||
<select id="aiCap">${AI_CAPS.map((c) => `<option value="${c}">${c}</option>`).join("")}</select>
|
||||
<div class="row"><button type="button" id="aiRunCap">Run capability</button></div>
|
||||
<div class="outbox empty" id="aiOut">—</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="work-panel" style="min-height:min(70vh,640px);display:flex;flex-direction:column">
|
||||
<div class="wp-head">
|
||||
<span class="ink-brand">◈ INK</span>
|
||||
<span class="muted" style="margin-left:8px;font-size:0.75rem;font-weight:500;text-transform:none;letter-spacing:0">neural operator</span>
|
||||
</div>
|
||||
<div class="work-panel">
|
||||
<div class="wp-head">Configure LLM (BYO)</div>
|
||||
<div class="wp-body">${llmFormHtml("llm")}</div>
|
||||
<div class="wp-body" style="display:flex;flex-direction:column;flex:1;min-height:0">
|
||||
<p class="muted" style="font-size:0.78rem;margin:0 0 8px">
|
||||
Chat with the op. INK inspects sessions, listeners, events, and runs railed actions
|
||||
(e.g. <em>“setup reverse shell on 4444”</em>). Configure models under <strong>Admin</strong>.
|
||||
</p>
|
||||
<label>LLM connection</label>
|
||||
<select id="aiLlmPick"><option value="">Loading…</option></select>
|
||||
<div id="aiPageLog" class="outbox" style="flex:1;max-height:none;min-height:220px;margin-top:10px"></div>
|
||||
<label style="margin-top:10px">Message</label>
|
||||
<textarea id="aiData" rows="3" placeholder="Talk to INK… Enter to send, Shift+Enter for newline"></textarea>
|
||||
<div class="row">
|
||||
<button type="button" class="primary" id="aiRun">Send</button>
|
||||
<button type="button" id="aiClearPage">Clear</button>
|
||||
<button type="button" id="aiStatus">Status</button>
|
||||
<button type="button" id="aiTools">Tools</button>
|
||||
<button type="button" id="aiOpenDrawer">Pop-out</button>
|
||||
</div>
|
||||
<div class="outbox empty hidden" id="aiOut">—</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
bindLlmForm("llm");
|
||||
const refreshPick = async () => {
|
||||
const llms = await loadSavedLlms();
|
||||
const prev = el("aiLlmPick")?.value || loadSel("sc5_ops_llm") || "";
|
||||
@@ -1147,7 +1137,7 @@
|
||||
saveSel("sc5_ops_llm", el("aiLlmPick").value || "");
|
||||
if (el("aiLlmGlobal") && el("aiLlmPick").value) el("aiLlmGlobal").value = el("aiLlmPick").value;
|
||||
};
|
||||
if (el("aiRun")) el("aiRun").onclick = async () => {
|
||||
const sendPage = async () => {
|
||||
const msg = (el("aiData") && el("aiData").value) || "";
|
||||
if (!msg.trim()) return showError("Enter a message");
|
||||
el("aiRun").disabled = true;
|
||||
@@ -1156,20 +1146,28 @@
|
||||
if (el("aiData")) el("aiData").value = "";
|
||||
} finally { el("aiRun").disabled = false; }
|
||||
};
|
||||
if (el("aiRun")) el("aiRun").onclick = sendPage;
|
||||
if (el("aiData")) {
|
||||
el("aiData").onkeydown = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendPage();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (el("aiClearPage")) el("aiClearPage").onclick = () => clearAiChat();
|
||||
if (el("aiRunCap")) el("aiRunCap").onclick = () => runAiCapability(el("aiCap").value, el("aiData").value, "aiOut");
|
||||
if (el("aiStatus")) el("aiStatus").onclick = async () => {
|
||||
try {
|
||||
const r = await api("GET", "/api/v1/ai/status");
|
||||
el("aiOut").classList.remove("hidden", "empty");
|
||||
el("aiOut").textContent = JSON.stringify(r, null, 2);
|
||||
el("aiOut").classList.remove("empty");
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("aiTools")) el("aiTools").onclick = async () => {
|
||||
try {
|
||||
const r = await api("GET", "/api/v1/ai/tools");
|
||||
el("aiOut").classList.remove("hidden", "empty");
|
||||
el("aiOut").textContent = JSON.stringify(r, null, 2);
|
||||
el("aiOut").classList.remove("empty");
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("aiOpenDrawer")) el("aiOpenDrawer").onclick = () => openAiDrawer(true);
|
||||
@@ -1205,7 +1203,7 @@
|
||||
if (tools.some((t) => t.ok && /listener|session|task|payload/i.test(t.tool || ""))) {
|
||||
if (window.__SC5_refresh) try { await window.__SC5_refresh(); } catch (_) {}
|
||||
}
|
||||
showOk(r.mode === "offline" ? "AI (offline tools)" : "AI reply");
|
||||
showOk(r.mode === "offline" ? "INK (offline)" : "INK replied");
|
||||
return r;
|
||||
} catch (e) {
|
||||
const err = String(e.message || e);
|
||||
@@ -1215,26 +1213,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function runAiCapability(capability, user_data, outId) {
|
||||
try {
|
||||
const body = {
|
||||
capability: capability || "recon_assist",
|
||||
user_data: user_data || "",
|
||||
};
|
||||
const llm_id = selectedLlmId();
|
||||
if (llm_id) body.llm_id = llm_id;
|
||||
const r = await api("POST", "/api/v1/ai/run", body);
|
||||
const text = typeof r === "string" ? r : JSON.stringify(r, null, 2);
|
||||
if (outId && el(outId)) {
|
||||
el(outId).textContent = text;
|
||||
el(outId).classList.remove("empty");
|
||||
}
|
||||
showOk("Capability complete");
|
||||
return r;
|
||||
} catch (e) {
|
||||
showError(String(e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function appendAiChat(who, text, toolTrace) {
|
||||
const log = el("aiChatLog");
|
||||
@@ -1245,7 +1223,7 @@
|
||||
div.className = "ai-msg " + (who === "user" ? "user" : "bot");
|
||||
const w = document.createElement("div");
|
||||
w.className = "who";
|
||||
w.textContent = who === "user" ? "You" : "Admin AI";
|
||||
w.textContent = who === "user" ? "You" : "INK";
|
||||
const b = document.createElement("div");
|
||||
b.style.whiteSpace = "pre-wrap";
|
||||
b.textContent = text;
|
||||
@@ -1390,7 +1368,14 @@
|
||||
</div>` : ""}
|
||||
</div>
|
||||
`;
|
||||
if (can("llm:manage") || can("admin")) bindLlmForm("adLlm");
|
||||
if (can("llm:manage") || can("admin")) {
|
||||
bindLlmForm("adLlm");
|
||||
window.__SC5_refreshLlms = async () => {
|
||||
const llms = await loadSavedLlms();
|
||||
fillLlmSelect(el("aiLlmGlobal"), llms, loadSel("sc5_ops_llm") || "");
|
||||
fillLlmSelect(el("aiLlmPick"), llms, loadSel("sc5_ops_llm") || "");
|
||||
};
|
||||
}
|
||||
function selectedScopes() {
|
||||
return Array.from(document.querySelectorAll(".ad-scope:checked")).map((c) => c.value);
|
||||
}
|
||||
|
||||
+283
-49
@@ -61,6 +61,28 @@
|
||||
.hidden { display: none !important; }
|
||||
a { color: var(--pink2); }
|
||||
|
||||
/* Themed scrollbars */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(233,30,140,0.45) rgba(255,255,255,0.04);
|
||||
}
|
||||
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
*::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 999px;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, rgba(233,30,140,0.55), rgba(167,139,250,0.4));
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, rgba(244,114,182,0.75), rgba(167,139,250,0.55));
|
||||
background-clip: padding-box;
|
||||
}
|
||||
*::-webkit-scrollbar-corner { background: transparent; }
|
||||
|
||||
/* —— App shell —— */
|
||||
.app {
|
||||
display: grid;
|
||||
@@ -121,7 +143,27 @@
|
||||
.pill.live { color: var(--pink2); border-color: rgba(233,30,140,0.4); }
|
||||
.top button { padding: 5px 10px; font-size: 0.78rem; }
|
||||
|
||||
/* Sidebar */
|
||||
/* Sidebar / mobile drawer */
|
||||
.nav-burger {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
width: 40px; height: 36px; padding: 0;
|
||||
align-items: center; justify-content: center;
|
||||
flex-direction: column; gap: 5px;
|
||||
}
|
||||
.nav-burger span {
|
||||
display: block; width: 18px; height: 2px;
|
||||
background: var(--text); border-radius: 2px;
|
||||
transition: transform 0.15s, opacity 0.15s;
|
||||
}
|
||||
.app.nav-open .nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.app.nav-open .nav-burger span:nth-child(2) { opacity: 0; }
|
||||
.app.nav-open .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
.nav-backdrop {
|
||||
display: none; position: fixed; inset: 0; z-index: 54;
|
||||
background: rgba(0,0,0,0.55);
|
||||
}
|
||||
.nav-backdrop.show { display: block; }
|
||||
.side {
|
||||
grid-area: side;
|
||||
background: var(--bg2);
|
||||
@@ -210,20 +252,63 @@
|
||||
background: rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
/* Bottom console */
|
||||
/* Bottom console — resizable height + pane split */
|
||||
.bottom {
|
||||
grid-area: bottom;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #0a0a0e;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
.bottom-resize {
|
||||
flex: 0 0 10px;
|
||||
cursor: ns-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 6;
|
||||
touch-action: none;
|
||||
}
|
||||
.bottom-resize::after {
|
||||
content: "";
|
||||
position: absolute; left: 50%; top: 3px;
|
||||
width: 48px; height: 3px; margin-left: -24px;
|
||||
border-radius: 999px;
|
||||
background: rgba(233,30,140,0.45);
|
||||
}
|
||||
.bottom-resize:hover::after,
|
||||
.bottom-resize.dragging::after {
|
||||
background: rgba(244,114,182,0.9);
|
||||
box-shadow: 0 0 10px rgba(233,30,140,0.45);
|
||||
}
|
||||
.bottom-panes {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: var(--pane-split, 1fr 6px 1fr);
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.bottom-pane {
|
||||
display: flex; flex-direction: column; min-height: 0; min-width: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.bottom-pane:last-child { border-right: 0; }
|
||||
.pane-vsplit {
|
||||
cursor: ew-resize;
|
||||
background: rgba(255,255,255,0.06);
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
z-index: 5;
|
||||
}
|
||||
.pane-vsplit::after {
|
||||
content: "";
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
width: 3px; height: 28px; margin: -14px 0 0 -1.5px;
|
||||
border-radius: 999px; background: rgba(233,30,140,0.4);
|
||||
}
|
||||
.pane-vsplit:hover::after,
|
||||
.pane-vsplit.dragging::after {
|
||||
background: rgba(244,114,182,0.9);
|
||||
}
|
||||
.bottom-pane .bp-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 4px 10px; border-bottom: 1px solid var(--border);
|
||||
@@ -238,6 +323,12 @@
|
||||
flex: 1; overflow: auto; padding: 6px 10px;
|
||||
font-family: var(--mono); font-size: 0.72rem; line-height: 1.4;
|
||||
color: #c4c4d0; white-space: pre-wrap; word-break: break-word;
|
||||
min-height: 0;
|
||||
}
|
||||
.ink-brand {
|
||||
background: linear-gradient(90deg, var(--pink2), var(--purple));
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent; font-weight: 800; letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Content primitives */
|
||||
@@ -463,11 +554,10 @@
|
||||
}
|
||||
body { overflow: hidden; }
|
||||
.app {
|
||||
grid-template-rows: var(--top-h) var(--nav-h) 1fr var(--bottom-h);
|
||||
grid-template-rows: var(--top-h) 1fr var(--bottom-h);
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"side"
|
||||
"main"
|
||||
"bottom";
|
||||
height: 100dvh;
|
||||
@@ -476,24 +566,21 @@
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
.app.no-bottom {
|
||||
grid-template-rows: var(--top-h) var(--nav-h) 1fr;
|
||||
grid-template-rows: var(--top-h) 1fr;
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"side"
|
||||
"main";
|
||||
}
|
||||
.app.no-ctx {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"side"
|
||||
"main"
|
||||
"bottom";
|
||||
}
|
||||
.app.no-bottom.no-ctx {
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"side"
|
||||
"main";
|
||||
}
|
||||
|
||||
@@ -532,33 +619,44 @@
|
||||
}
|
||||
.top #btnRefresh { min-width: 36px; padding: 0 8px; }
|
||||
|
||||
/* —— Horizontal nav (easy swipe scroll) —— */
|
||||
/* —— Hamburger drawer nav —— */
|
||||
.nav-burger { display: inline-flex; }
|
||||
.side {
|
||||
position: sticky; top: var(--top-h); z-index: 45;
|
||||
flex-direction: row; border-right: 0; border-bottom: 1px solid var(--border);
|
||||
overflow: hidden; height: var(--nav-h); max-height: var(--nav-h);
|
||||
position: fixed;
|
||||
top: 0; left: 0; bottom: 0;
|
||||
width: min(300px, 86vw);
|
||||
z-index: 55;
|
||||
border-right: 1px solid var(--border2);
|
||||
box-shadow: 16px 0 40px rgba(0,0,0,0.45);
|
||||
transform: translateX(-105%);
|
||||
transition: transform 0.22s ease;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
background: var(--bg2);
|
||||
}
|
||||
.app.nav-open .side { transform: translateX(0); }
|
||||
.side-nav {
|
||||
flex-direction: row; align-items: center;
|
||||
padding: 4px 8px; gap: 6px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
flex-direction: column;
|
||||
padding: 12px 10px;
|
||||
gap: 4px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-snap-type: x proximity;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.side-nav::-webkit-scrollbar { display: none; }
|
||||
.nav-item {
|
||||
white-space: nowrap; flex: 0 0 auto;
|
||||
padding: 8px 14px; min-height: 38px;
|
||||
font-size: 0.8rem; scroll-snap-align: start;
|
||||
width: 100%;
|
||||
padding: 14px 14px;
|
||||
min-height: 48px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.nav-item .badge-n { display: none; }
|
||||
.nav-item .ico { display: none; }
|
||||
.side-foot { display: none; }
|
||||
.nav-item .ico { display: inline-block; font-size: 1.05rem; }
|
||||
.nav-item .badge-n { display: inline-block; }
|
||||
.side-foot {
|
||||
display: block;
|
||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
/* —— Context as scrollable bottom sheet —— */
|
||||
.ctx {
|
||||
@@ -633,11 +731,19 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-height: 0;
|
||||
}
|
||||
.bottom {
|
||||
.bottom { min-height: 0; }
|
||||
.bottom-panes {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 0;
|
||||
grid-template-rows: var(--pane-stack, 1fr 6px 1fr);
|
||||
}
|
||||
.pane-vsplit {
|
||||
cursor: ns-resize;
|
||||
width: auto; height: 6px;
|
||||
}
|
||||
.pane-vsplit::after {
|
||||
width: 36px; height: 3px; top: 50%; left: 50%;
|
||||
margin: -1.5px 0 0 -18px;
|
||||
}
|
||||
.bottom-pane:first-child { border-right: 0; border-bottom: 1px solid var(--border); max-height: 50%; }
|
||||
.bottom-pane .bp-body {
|
||||
font-size: 0.68rem;
|
||||
overflow-y: auto;
|
||||
@@ -684,6 +790,9 @@
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<header class="top">
|
||||
<button type="button" class="nav-burger" id="navBurger" title="Menu" aria-label="Open navigation" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<img class="logo" src="/ops/assets/squidsec-logo-96.png" alt=""
|
||||
onerror="this.src='/ops/assets/squidsec-logo-256.png'" />
|
||||
<div class="title">SQUID<span>C5</span></div>
|
||||
@@ -697,7 +806,8 @@
|
||||
<button type="button" id="btnRefresh" title="Refresh data">↻</button>
|
||||
</header>
|
||||
|
||||
<nav class="side" aria-label="Primary">
|
||||
<div class="nav-backdrop" id="navBackdrop" hidden></div>
|
||||
<nav class="side" id="sideNavPanel" aria-label="Primary">
|
||||
<div class="side-nav" id="sideNav">
|
||||
<button type="button" class="nav-item active" data-view="dashboard"><span class="ico">▣</span> Dashboard</button>
|
||||
<button type="button" class="nav-item" data-view="sessions"><span class="ico">◉</span> Sessions <span class="badge-n" id="navNSes">0</span></button>
|
||||
@@ -705,7 +815,7 @@
|
||||
<button type="button" class="nav-item" data-view="payloads"><span class="ico">⌘</span> Payloads</button>
|
||||
<button type="button" class="nav-item" data-view="postex"><span class="ico">⚡</span> Post-Ex</button>
|
||||
<button type="button" class="nav-item" data-view="collab"><span class="ico">⇄</span> Collab</button>
|
||||
<button type="button" class="nav-item" data-view="ai" id="navAi"><span class="ico">✦</span> AI</button>
|
||||
<button type="button" class="nav-item" data-view="ai" id="navAi"><span class="ico">◈</span> INK</button>
|
||||
<button type="button" class="nav-item" data-view="observe"><span class="ico">▤</span> Observe</button>
|
||||
<button type="button" class="nav-item hidden" data-view="admin" id="navAdmin"><span class="ico">⚙</span> Admin</button>
|
||||
</div>
|
||||
@@ -760,22 +870,24 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global AI assistant (all pages) -->
|
||||
<button type="button" id="aiFab" class="ai-fab hidden" title="Admin AI assistant">✦ AI</button>
|
||||
<div id="aiDrawer" class="ai-drawer hidden" aria-label="Admin AI chat">
|
||||
<!-- INK — SquidC5 neural operator (global) -->
|
||||
<button type="button" id="aiFab" class="ai-fab hidden" title="INK — neural operator">◈ INK</button>
|
||||
<div id="aiDrawer" class="ai-drawer hidden" aria-label="INK chat">
|
||||
<div class="ai-drawer-head">
|
||||
<strong>Admin AI</strong>
|
||||
<strong class="ink-brand">◈ INK</strong>
|
||||
<span class="muted" style="font-size:0.68rem;font-weight:500">neural operator</span>
|
||||
<a class="doc-link" href="https://github.com/SquidSec/SquidC5/blob/master/docs/user-guide.md#admin-ai" target="_blank" rel="noopener noreferrer" style="font-size:0.7rem">Docs</a>
|
||||
<button type="button" id="aiDrawerClose" style="margin-left:auto">✕</button>
|
||||
</div>
|
||||
<div class="ai-drawer-body" id="aiChatLog"></div>
|
||||
<div class="ai-drawer-foot">
|
||||
<select id="aiLlmGlobal" title="LLM connection"></select>
|
||||
<textarea id="aiPromptGlobal" rows="3" placeholder="Ask anything — e.g. list sessions, setup reverse shell on 4444…"></textarea>
|
||||
<textarea id="aiPromptGlobal" rows="3" placeholder="Ask INK anything — list sessions, spin up a listener…"></textarea>
|
||||
<div class="row" style="margin:0;gap:6px">
|
||||
<button type="button" class="primary" id="aiSendGlobal" style="flex:1">Send</button>
|
||||
<button type="button" id="aiClearGlobal" title="Clear chat">Clear</button>
|
||||
</div>
|
||||
<p class="muted" style="margin:0;font-size:0.62rem">Enter to send · Shift+Enter newline</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -791,14 +903,18 @@
|
||||
</aside>
|
||||
<div class="ctx-backdrop hidden" id="ctxBackdrop" aria-hidden="true"></div>
|
||||
|
||||
<footer class="bottom">
|
||||
<div class="bottom-pane">
|
||||
<div class="bp-head"><span class="live-dot"></span> Event stream <span class="muted" id="eventCount" style="margin-left:auto;text-transform:none;letter-spacing:0">0</span></div>
|
||||
<div class="bp-body" id="eventsBox">Connect to stream events…</div>
|
||||
</div>
|
||||
<div class="bottom-pane">
|
||||
<div class="bp-head">Output / console</div>
|
||||
<div class="bp-body" id="consoleBox">Command and task output appears here.</div>
|
||||
<footer class="bottom" id="bottomDock">
|
||||
<div class="bottom-resize" id="bottomResize" title="Drag to resize panel height" role="separator" aria-orientation="horizontal"></div>
|
||||
<div class="bottom-panes" id="bottomPanes">
|
||||
<div class="bottom-pane" id="paneEvents">
|
||||
<div class="bp-head"><span class="live-dot"></span> Event stream <span class="muted" id="eventCount" style="margin-left:auto;text-transform:none;letter-spacing:0">0</span></div>
|
||||
<div class="bp-body" id="eventsBox">Connect to stream events…</div>
|
||||
</div>
|
||||
<div class="pane-vsplit" id="paneSplit" title="Drag to resize panes" role="separator" aria-orientation="vertical"></div>
|
||||
<div class="bottom-pane" id="paneConsole">
|
||||
<div class="bp-head">Output / console</div>
|
||||
<div class="bp-body" id="consoleBox">Command and task output appears here.</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -933,11 +1049,35 @@
|
||||
payloads: { title: "Payloads", sub: "Generate implants and templates", doc: "payloads-and-implants" },
|
||||
postex: { title: "Post-Ex", sub: "Files, SOCKS, modules on selected session", doc: "file-ops" },
|
||||
collab: { title: "Collab", sub: "Teams, chat, handoff, presence", doc: "multi-operator-collab" },
|
||||
ai: { title: "Admin AI", sub: "Sandboxed allow-listed AI capabilities", doc: "admin-ai" },
|
||||
ai: { title: "INK", sub: "Neural operator — chat, inspect, and drive the C5", doc: "admin-ai" },
|
||||
observe: { title: "Observe", sub: "Metrics, audit, timeline, reports", doc: "timeline-and-reports" },
|
||||
admin: { title: "Admin", sub: "Tokens, policy, features, LLM, MCP", doc: "feature-toggles" },
|
||||
};
|
||||
let currentView = "dashboard";
|
||||
function closeNavDrawer() {
|
||||
const app = $("app");
|
||||
if (app) app.classList.remove("nav-open");
|
||||
const b = $("navBurger");
|
||||
if (b) b.setAttribute("aria-expanded", "false");
|
||||
const bd = $("navBackdrop");
|
||||
if (bd) { bd.classList.remove("show"); bd.hidden = true; }
|
||||
}
|
||||
function openNavDrawer() {
|
||||
const app = $("app");
|
||||
if (app) app.classList.add("nav-open");
|
||||
const b = $("navBurger");
|
||||
if (b) b.setAttribute("aria-expanded", "true");
|
||||
const bd = $("navBackdrop");
|
||||
if (bd) { bd.hidden = false; bd.classList.add("show"); }
|
||||
}
|
||||
function toggleNavDrawer() {
|
||||
const app = $("app");
|
||||
if (app && app.classList.contains("nav-open")) closeNavDrawer();
|
||||
else openNavDrawer();
|
||||
}
|
||||
if ($("navBurger")) $("navBurger").onclick = (e) => { e.stopPropagation(); toggleNavDrawer(); };
|
||||
if ($("navBackdrop")) $("navBackdrop").onclick = () => closeNavDrawer();
|
||||
|
||||
function setView(name) {
|
||||
if (name === "admin" && !can("admin") && !can("tokens:manage") && !can("policy:manage")) name = "dashboard";
|
||||
if (name === "ai" && !can("ai:use") && !can("admin")) name = "dashboard";
|
||||
@@ -950,7 +1090,6 @@
|
||||
});
|
||||
const app = document.querySelector(".app");
|
||||
if (app) {
|
||||
// Admin: full width — hide session context + bottom event/output panes
|
||||
const focusAdmin = name === "admin";
|
||||
const focusWide = name === "admin" || name === "ai";
|
||||
app.classList.toggle("no-ctx", focusWide);
|
||||
@@ -960,6 +1099,7 @@
|
||||
$("viewTitle").textContent = meta.title;
|
||||
$("viewSub").textContent = meta.sub;
|
||||
$("viewTools").innerHTML = "";
|
||||
closeNavDrawer();
|
||||
if (window.__SC5_onView) window.__SC5_onView(name);
|
||||
if (window.__SC5_setPageDocs) window.__SC5_setPageDocs(name);
|
||||
try { localStorage.setItem("sc5_ops_view", name); } catch (_) {}
|
||||
@@ -969,6 +1109,100 @@
|
||||
b.addEventListener("click", () => setView(b.getAttribute("data-view")));
|
||||
});
|
||||
|
||||
/* —— Resizable bottom dock —— */
|
||||
(function setupBottomResize() {
|
||||
const root = document.documentElement;
|
||||
const app = $("app");
|
||||
const hHandle = $("bottomResize");
|
||||
const vHandle = $("paneSplit");
|
||||
function loadH() {
|
||||
try {
|
||||
const v = parseInt(localStorage.getItem("sc5_bottom_h") || "", 10);
|
||||
if (v >= 80 && v <= Math.floor(window.innerHeight * 0.7)) {
|
||||
root.style.setProperty("--bottom-h", v + "px");
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
function loadSplit() {
|
||||
try {
|
||||
const r = parseFloat(localStorage.getItem("sc5_pane_ratio") || "");
|
||||
if (r > 0.15 && r < 0.85) {
|
||||
const mobile = window.matchMedia("(max-width: 900px)").matches;
|
||||
if (mobile) {
|
||||
root.style.setProperty("--pane-stack", r + "fr 6px " + (1 - r) + "fr");
|
||||
} else {
|
||||
root.style.setProperty("--pane-split", r + "fr 6px " + (1 - r) + "fr");
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
loadH();
|
||||
loadSplit();
|
||||
window.addEventListener("resize", () => loadSplit());
|
||||
|
||||
function drag(handle, onMove, onEnd) {
|
||||
if (!handle) return;
|
||||
let active = false;
|
||||
const start = (e) => {
|
||||
if (app && app.classList.contains("no-bottom")) return;
|
||||
active = true;
|
||||
handle.classList.add("dragging");
|
||||
e.preventDefault();
|
||||
const move = (ev) => {
|
||||
if (!active) return;
|
||||
const pt = ev.touches ? ev.touches[0] : ev;
|
||||
onMove(pt.clientX, pt.clientY);
|
||||
};
|
||||
const end = () => {
|
||||
active = false;
|
||||
handle.classList.remove("dragging");
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseup", end);
|
||||
window.removeEventListener("touchmove", move);
|
||||
window.removeEventListener("touchend", end);
|
||||
if (onEnd) onEnd();
|
||||
};
|
||||
window.addEventListener("mousemove", move);
|
||||
window.addEventListener("mouseup", end);
|
||||
window.addEventListener("touchmove", move, { passive: false });
|
||||
window.addEventListener("touchend", end);
|
||||
};
|
||||
handle.addEventListener("mousedown", start);
|
||||
handle.addEventListener("touchstart", start, { passive: false });
|
||||
}
|
||||
|
||||
drag(hHandle, (_x, y) => {
|
||||
const h = Math.max(80, Math.min(window.innerHeight * 0.7, window.innerHeight - y));
|
||||
root.style.setProperty("--bottom-h", Math.round(h) + "px");
|
||||
}, () => {
|
||||
try {
|
||||
const cur = getComputedStyle(root).getPropertyValue("--bottom-h").trim();
|
||||
const n = parseInt(cur, 10);
|
||||
if (n) localStorage.setItem("sc5_bottom_h", String(n));
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
drag(vHandle, (x, y) => {
|
||||
const panes = $("bottomPanes");
|
||||
if (!panes) return;
|
||||
const rect = panes.getBoundingClientRect();
|
||||
const mobile = window.matchMedia("(max-width: 900px)").matches;
|
||||
let r;
|
||||
if (mobile) {
|
||||
r = (y - rect.top) / rect.height;
|
||||
} else {
|
||||
r = (x - rect.left) / rect.width;
|
||||
}
|
||||
r = Math.max(0.18, Math.min(0.82, r));
|
||||
if (mobile) {
|
||||
root.style.setProperty("--pane-stack", r + "fr 6px " + (1 - r) + "fr");
|
||||
} else {
|
||||
root.style.setProperty("--pane-split", r + "fr 6px " + (1 - r) + "fr");
|
||||
}
|
||||
try { localStorage.setItem("sc5_pane_ratio", String(r)); } catch (_) {}
|
||||
});
|
||||
})();
|
||||
|
||||
/* —— Connect modal —— */
|
||||
function openModal() { $("connectModal").classList.add("show"); }
|
||||
function closeModal() { $("connectModal").classList.remove("show"); }
|
||||
|
||||
Reference in New Issue
Block a user