mirror of
https://github.com/SquidSec/SquidC5
synced 2026-08-09 12:22:26 +00:00
fix(ops): shell Run on Session tab + larger output + UI polish
Wire context handlers per mount so duplicate ctxRun IDs no longer leave the Session-tab Run button dead (getElementById hit the hidden rail). Grow session/console output space, right-align Close, restyle INKO close.
This commit is contained in:
+65
-31
@@ -557,27 +557,63 @@
|
||||
}
|
||||
bindCtxChrome();
|
||||
|
||||
function bindContextHandlers() {
|
||||
if (el("ctxClaim")) el("ctxClaim").onclick = async () => {
|
||||
function bindContextHandlers(root) {
|
||||
/* Scope to mount root — multiple panels mirror the same markup; document.getElementById
|
||||
would only wire the first (often hidden ctx rail) and leave Session-tab Run dead. */
|
||||
const scope = root && root.querySelector ? root : document;
|
||||
const q = (sel) => scope.querySelector(sel);
|
||||
const setOut = (text) => {
|
||||
const box = q("#ctxOut");
|
||||
if (!box) return;
|
||||
box.textContent = text == null || text === "" ? "(no output)" : String(text);
|
||||
box.classList.remove("empty");
|
||||
};
|
||||
const runShell = async () => {
|
||||
const cmdEl = q("#ctxCmd");
|
||||
const command = (cmdEl && cmdEl.value || "").trim();
|
||||
if (!command) return showError("Command required");
|
||||
if (!selectedId) return showError("Select a session");
|
||||
const btn = q("#ctxRun");
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const r = await api("POST", "/api/v1/shell/command", {
|
||||
session_id: selectedId,
|
||||
command,
|
||||
wait_sec: 8,
|
||||
});
|
||||
const out = r.output != null ? r.output : (r.result != null ? r.result : JSON.stringify(r, null, 2));
|
||||
if (typeof showOutput === "function") showOutput(out, "$ " + command);
|
||||
else if (window.__SC5_showOutput) window.__SC5_showOutput(out, "$ " + command);
|
||||
setOut(out);
|
||||
if (typeof window.__SC5_focusConsole === "function") window.__SC5_focusConsole();
|
||||
showOk("Shell command sent");
|
||||
} catch (e) {
|
||||
showError(String(e.message || e));
|
||||
setOut("Error: " + String(e.message || e));
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
};
|
||||
if (q("#ctxClaim")) q("#ctxClaim").onclick = async () => {
|
||||
try {
|
||||
const r = await api("POST", `/api/v1/sessions/${encodeURIComponent(selectedId)}/claim`, {});
|
||||
showOk("Lock claimed");
|
||||
if (el("ctxOut")) { el("ctxOut").textContent = JSON.stringify(r, null, 2); el("ctxOut").classList.remove("empty"); }
|
||||
setOut(JSON.stringify(r, null, 2));
|
||||
if (window.__SC5_refresh) await window.__SC5_refresh();
|
||||
renderContext(true);
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxForceClaim")) el("ctxForceClaim").onclick = async () => {
|
||||
if (q("#ctxForceClaim")) q("#ctxForceClaim").onclick = async () => {
|
||||
if (!(await askConfirm("Force-steal lock from current holder?"))) return;
|
||||
try {
|
||||
const r = await api("POST", `/api/v1/sessions/${encodeURIComponent(selectedId)}/claim`, { force: true });
|
||||
showOk("Force claimed");
|
||||
if (el("ctxOut")) { el("ctxOut").textContent = JSON.stringify(r, null, 2); el("ctxOut").classList.remove("empty"); }
|
||||
setOut(JSON.stringify(r, null, 2));
|
||||
if (window.__SC5_refresh) await window.__SC5_refresh();
|
||||
renderContext(true);
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxRelease")) el("ctxRelease").onclick = async () => {
|
||||
if (q("#ctxRelease")) q("#ctxRelease").onclick = async () => {
|
||||
try {
|
||||
await api("POST", `/api/v1/sessions/${encodeURIComponent(selectedId)}/release`);
|
||||
showOk("Lock released");
|
||||
@@ -585,14 +621,14 @@
|
||||
renderContext(true);
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxSpectate")) el("ctxSpectate").onclick = async () => {
|
||||
if (q("#ctxSpectate")) q("#ctxSpectate").onclick = async () => {
|
||||
try {
|
||||
const r = await api("GET", `/api/v1/sessions/${encodeURIComponent(selectedId)}/spectator`);
|
||||
if (el("ctxOut")) { el("ctxOut").textContent = JSON.stringify(r, null, 2); el("ctxOut").classList.remove("empty"); }
|
||||
setOut(JSON.stringify(r, null, 2));
|
||||
showOk("Spectator snapshot");
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxStabilize")) el("ctxStabilize").onclick = async () => {
|
||||
if (q("#ctxStabilize")) q("#ctxStabilize").onclick = async () => {
|
||||
if (!selectedId) return;
|
||||
if (!(await askConfirm("Inject stage-2 stabilize agent? Detects Linux vs Windows and reconnects a durable channel."))) return;
|
||||
try {
|
||||
@@ -602,31 +638,29 @@
|
||||
{ os: "auto" },
|
||||
);
|
||||
showOk(r.status === "already_stable" ? "Already stable" : ("Stabilized as " + (r.os || "?")));
|
||||
if (el("ctxOut")) {
|
||||
el("ctxOut").textContent = JSON.stringify(r, null, 2);
|
||||
el("ctxOut").classList.remove("empty");
|
||||
}
|
||||
setOut(JSON.stringify(r, null, 2));
|
||||
if (window.__SC5_refresh) await window.__SC5_refresh();
|
||||
renderContext(true);
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxRun")) el("ctxRun").onclick = async () => {
|
||||
const command = (el("ctxCmd").value || "").trim();
|
||||
if (!command) return showError("Command required");
|
||||
try {
|
||||
const r = await api("POST", "/api/v1/shell/command", { session_id: selectedId, command, wait_sec: 5 });
|
||||
const out = r.output || r.result || JSON.stringify(r, null, 2);
|
||||
showOutput(out, "$ " + command);
|
||||
if (el("ctxOut")) { el("ctxOut").textContent = out; el("ctxOut").classList.remove("empty"); }
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
if (el("ctxTaskBtn")) el("ctxTaskBtn").onclick = async () => {
|
||||
const command = (el("ctxTask").value || "").trim();
|
||||
if (q("#ctxRun")) q("#ctxRun").onclick = () => { runShell(); };
|
||||
if (q("#ctxCmd")) {
|
||||
q("#ctxCmd").onkeydown = (ev) => {
|
||||
if (ev.key === "Enter" && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
runShell();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (q("#ctxTaskBtn")) q("#ctxTaskBtn").onclick = async () => {
|
||||
const taskEl = q("#ctxTask");
|
||||
const command = (taskEl && taskEl.value || "").trim();
|
||||
if (!command) return showError("Command required");
|
||||
try {
|
||||
const r = await api("POST", "/api/v1/tasks", { session_id: selectedId, command });
|
||||
showOk("Task " + (r.id || "queued"));
|
||||
if (el("ctxOut")) { el("ctxOut").textContent = JSON.stringify(r, null, 2); el("ctxOut").classList.remove("empty"); }
|
||||
setOut(JSON.stringify(r, null, 2));
|
||||
if (typeof showOutput === "function") showOutput(JSON.stringify(r, null, 2), "task " + (r.id || ""));
|
||||
} catch (e) { showError(String(e.message || e)); }
|
||||
};
|
||||
}
|
||||
@@ -702,7 +736,7 @@
|
||||
${shellTab}
|
||||
${taskTab}
|
||||
</div>
|
||||
<div class="outbox empty" id="ctxOut" style="max-height:160px;margin-top:10px;flex-shrink:0">Output</div>
|
||||
<div class="outbox empty ctx-outbox" id="ctxOut">Output</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -730,7 +764,7 @@
|
||||
body.innerHTML = ctxPanelHtml(s, claim);
|
||||
ctxBoundSid = selectedId;
|
||||
bindSubTabs(body);
|
||||
bindContextHandlers();
|
||||
bindContextHandlers(body);
|
||||
}
|
||||
|
||||
function bindSubTabs(root) {
|
||||
@@ -861,17 +895,17 @@
|
||||
<div class="toolbar">
|
||||
<span class="muted" id="sesCount">${rows.length} active</span>
|
||||
${can("sessions:write") ? '<button type="button" class="ghost sm" id="sesReap">Reap dead</button>' : ""}
|
||||
${can("sessions:write") ? '<button type="button" class="danger sm" id="sesClose">Close</button>' : ""}
|
||||
<button type="button" class="ghost sm" id="sesRefresh">Refresh</button>
|
||||
${can("sessions:write") ? '<button type="button" class="danger sm" id="sesClose" style="margin-left:auto">Close</button>' : ""}
|
||||
</div>
|
||||
<div style="flex:1;min-height:0;overflow:auto">
|
||||
${rows.length
|
||||
? `<table class="data"><thead><tr><th>Session</th><th>Kind</th><th>Host</th></tr></thead><tbody id="sesTbody"></tbody></table>`
|
||||
: '<div class="empty-state" id="sesEmpty"><strong>No sessions</strong>Land a beacon or reverse shell.</div>'}
|
||||
</div>
|
||||
<div id="sesDetail" class="outbox empty" style="max-height:100px;margin-top:8px;flex-shrink:0">Select a session...</div>
|
||||
<div id="sesDetail" class="outbox empty ses-detail-out">Select a session...</div>
|
||||
`},
|
||||
{ id: "sctx", label: "Session", html: `<div id="sesCtxMount" style="flex:1;min-height:0;overflow:auto"></div>` },
|
||||
{ id: "sctx", label: "Session", html: `<div id="sesCtxMount" class="ctx-mount" style="flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column"></div>` },
|
||||
{ id: "stasks", label: "Tasks", html: `
|
||||
<div class="toolbar">
|
||||
<button type="button" class="ghost sm" id="tskReload">Reload</button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
--purple: #a78bfa;
|
||||
--sidebar-w: 200px;
|
||||
--top-h: 48px;
|
||||
--bottom-h: 160px;
|
||||
--bottom-h: 280px;
|
||||
--font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--radius: 8px;
|
||||
@@ -54,11 +54,30 @@
|
||||
}
|
||||
button.accent:hover { background: rgba(52,211,153,0.22); }
|
||||
button.icon-close {
|
||||
width: 32px; height: 32px; min-height: 32px; padding: 0;
|
||||
width: 30px; height: 30px; min-height: 30px; min-width: 30px; padding: 0;
|
||||
border-radius: 8px; display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; line-height: 1; color: var(--muted);
|
||||
font-size: 1.05rem; line-height: 1; font-weight: 500;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
button.icon-close:hover {
|
||||
color: var(--text);
|
||||
border-color: rgba(233,30,140,0.45);
|
||||
background: rgba(233,30,140,0.1);
|
||||
}
|
||||
button.icon-close:active { background: rgba(233,30,140,0.16); }
|
||||
.ai-drawer-head-actions {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
flex-shrink: 0; margin-left: auto;
|
||||
}
|
||||
.ai-drawer-head-actions #aiNewChat {
|
||||
min-height: 30px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
button.icon-close:hover { color: var(--text); border-color: rgba(233,30,140,0.4); background: rgba(255,255,255,0.04); }
|
||||
button.ghost {
|
||||
background: transparent; border-color: var(--border); color: var(--muted); font-weight: 500;
|
||||
}
|
||||
@@ -427,6 +446,7 @@
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.page-tab-panels { flex: 1; min-height: 0; position: relative; overflow: hidden; }
|
||||
.page-tab-panel .ctx-mount { width: 100%; }
|
||||
.page-tab-panel {
|
||||
display: none !important;
|
||||
height: 100%; min-height: 0; overflow: auto; padding: 10px 12px;
|
||||
@@ -536,6 +556,24 @@
|
||||
}
|
||||
.ctx-compact .ctx-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
|
||||
.ctx-compact label { margin-top: 6px; }
|
||||
.ctx-compact {
|
||||
display: flex; flex-direction: column; min-height: 0; height: 100%;
|
||||
}
|
||||
.ctx-compact .ctx-outbox,
|
||||
.outbox.ctx-outbox {
|
||||
flex: 1 1 auto;
|
||||
min-height: 220px;
|
||||
max-height: none;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
.ses-detail-out {
|
||||
flex: 0 0 auto;
|
||||
min-height: 120px;
|
||||
max-height: 28vh;
|
||||
margin-top: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
/* Nested sub-tabs inside a page tab (e.g. Session → Shell / Task) */
|
||||
.sub-tabs { display: flex; flex-direction: column; flex: 1; min-height: 0; width: 100%; }
|
||||
.sub-tab-bar {
|
||||
@@ -836,7 +874,7 @@
|
||||
background: rgba(244,114,182,0.9);
|
||||
}
|
||||
.ai-drawer-head {
|
||||
display: flex; align-items: flex-start; gap: 8px; padding: 12px 12px 10px;
|
||||
display: flex; align-items: center; gap: 8px; padding: 12px 12px 10px;
|
||||
border-bottom: 1px solid var(--border); font-size: 0.85rem; flex-shrink: 0;
|
||||
padding-top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
@@ -1389,8 +1427,10 @@
|
||||
<strong class="inko-brand-full"><span class="inko-acronym">INKO</span></strong>
|
||||
<span class="muted" style="font-size:0.72rem;font-weight:600;line-height:1.25">Intelligent Neural Kinetic Operator</span>
|
||||
</div>
|
||||
<button type="button" class="accent sm" id="aiNewChat" title="Start a new chat" style="flex-shrink:0">New chat</button>
|
||||
<button type="button" class="icon-close" id="aiDrawerClose" style="margin-left:4px;flex-shrink:0" title="Close INKO" aria-label="Close INKO">×</button>
|
||||
<div class="ai-drawer-head-actions">
|
||||
<button type="button" class="accent sm" id="aiNewChat" title="Start a new chat">New chat</button>
|
||||
<button type="button" class="icon-close" id="aiDrawerClose" title="Close INKO" aria-label="Close INKO">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-drawer-body" id="aiChatLog"></div>
|
||||
<div class="ai-drawer-foot" id="aiDrawerFoot">
|
||||
@@ -1526,6 +1566,20 @@
|
||||
el.classList.add("show");
|
||||
setTimeout(() => el.classList.remove("show"), 3000);
|
||||
}
|
||||
function focusConsolePane() {
|
||||
const panes = $("bottomPanes");
|
||||
if (!panes) return;
|
||||
if (panes.classList.contains("mode-tabs")) {
|
||||
panes.querySelectorAll(".bottom-pane").forEach((p) => {
|
||||
p.classList.toggle("tab-active", p.getAttribute("data-bpane") === "console");
|
||||
});
|
||||
document.querySelectorAll("#bottomTabBar [data-bpane]").forEach((b) => {
|
||||
b.classList.toggle("active", b.getAttribute("data-bpane") === "console");
|
||||
});
|
||||
}
|
||||
const box = $("consoleBox");
|
||||
if (box) box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
function showOutput(text, meta) {
|
||||
const box = $("consoleBox");
|
||||
if (!box) return;
|
||||
@@ -1537,7 +1591,9 @@
|
||||
}
|
||||
box.textContent = head ? head + "\n\n" + body : body;
|
||||
box.scrollTop = box.scrollHeight;
|
||||
focusConsolePane();
|
||||
}
|
||||
window.__SC5_focusConsole = focusConsolePane;
|
||||
const PRIVACY_KEY = "sc5_ops_privacy";
|
||||
function privacyModeOn() {
|
||||
try { return localStorage.getItem(PRIVACY_KEY) === "1"; } catch (_) { return false; }
|
||||
|
||||
Reference in New Issue
Block a user