add deadmans key, fix downloading randomly stopping halfway through, add scripts to context menu.

This commit is contained in:
KingKDot
2026-05-16 16:33:40 -05:00
parent 089cf20bed
commit e2ef4a375f
6 changed files with 337 additions and 40 deletions
+68
View File
@@ -537,6 +537,63 @@ function renderPluginMenu(plugins) {
}
}
function renderScriptMenu(scripts) {
const section = document.getElementById("script-section");
const container = document.getElementById("script-menu");
if (!section || !container) return;
container.innerHTML = "";
if (!Array.isArray(scripts) || scripts.length === 0) {
section.classList.add("hidden");
return;
}
section.classList.remove("hidden");
const sorted = scripts.slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
for (const script of sorted) {
const btn = document.createElement("button");
btn.className = "ctx-item ctx-plugin-item";
btn.dataset.scriptId = script.id;
btn.title = `${script.scriptType} — click to open in Scripts with this client preselected`;
const labelIcon = document.createElement("i");
labelIcon.className = "fa-solid fa-scroll ctx-icon text-cyan-400";
btn.appendChild(labelIcon);
const label = document.createElement("span");
label.className = "ctx-plugin-label";
label.textContent = script.name || script.id;
btn.appendChild(label);
const badge = document.createElement("span");
badge.className = "ctx-plugin-badge";
badge.textContent = script.scriptType || "";
btn.appendChild(badge);
container.appendChild(btn);
}
}
let _scriptMenuCache = null;
async function loadSavedScriptsForMenu() {
const section = document.getElementById("script-section");
const container = document.getElementById("script-menu");
if (!section || !container) return;
if (_scriptMenuCache) renderScriptMenu(_scriptMenuCache);
try {
const res = await fetch("/api/saved-scripts");
if (!res.ok) return;
const data = await res.json();
const scripts = Array.isArray(data.items) ? data.items : [];
_scriptMenuCache = scripts;
renderScriptMenu(scripts);
} catch {
// ignore — cached render (if any) is already showing
}
}
async function loadPluginsForClient(clientId) {
const section = document.getElementById("plugin-section");
const container = document.getElementById("plugin-menu");
@@ -599,6 +656,7 @@ function initializeRenderer() {
const notificationsMuted = card?.dataset.notificationsMuted === "true";
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id), notificationsMuted });
loadPluginsForClient(id);
loadSavedScriptsForMenu();
},
openModal,
requestPreview,
@@ -1362,6 +1420,16 @@ menu.addEventListener("click", async (e) => {
closeMenu(clearContext);
return;
}
const scriptId = target.dataset.scriptId;
if (scriptId) {
window.open(
`/scripts?clientId=${encodeURIComponent(contextCard)}&scriptId=${encodeURIComponent(scriptId)}`,
"_blank",
"noopener",
);
closeMenu(clearContext);
return;
}
const open = target.dataset.open;
if (open === "console") {
window.open(`/${contextCard}/console`, "_blank", "noopener");
+35 -2
View File
@@ -746,9 +746,42 @@ function renderTemplatePalette() {
}
}
function applyContextMenuParams() {
const params = new URLSearchParams(window.location.search);
const clientId = params.get("clientId");
const scriptId = params.get("scriptId");
if (!clientId && !scriptId) return;
if (clientId) {
const found = allClients.find((c) => c.id === clientId);
if (found) {
selectedClients.clear();
selectedClients.add(clientId);
renderClients();
} else {
showToast(`Client ${clientId.substring(0, 8)} is not online`, "warning", 4000);
}
}
if (scriptId) {
const script = cachedSavedScripts.find((s) => s.id === scriptId);
if (script) {
setEditorValue(script.content);
scriptType.value = script.scriptType;
scriptSaveName.value = script.name;
setEditorMode(script.scriptType);
showToast(`Loaded "${script.name}" — ready to run`, "success", 3000);
} else {
showToast("Saved script not found", "error", 4000);
}
}
}
checkAuth();
loadClients();
loadSavedScripts().then(() => renderSavedScripts());
Promise.all([
loadClients(),
loadSavedScripts().then(() => renderSavedScripts()),
]).then(applyContextMenuParams);
loadAutoTasks();
renderTemplatePalette();
+2
View File
@@ -241,12 +241,14 @@ const mainRowsHTML =
`<button class="ctx-row" data-group-toggle="${g.id}"><i class="${g.icon} ctx-icon ${g.color}"></i><span style="flex:1">${g.label}</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`
).join("") +
`<div class="ctx-divider"></div>` +
`<button class="ctx-row hidden" id="script-section" data-group-toggle="scripts"><i class="fa-solid fa-scroll ctx-icon text-cyan-400"></i><span style="flex:1">Run Script</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>` +
`<button class="ctx-row hidden" id="plugin-section" data-group-toggle="plugins"><i class="fa-solid fa-puzzle-piece ctx-icon text-fuchsia-400"></i><span style="flex:1">Plugins</span><i class="fa-solid fa-chevron-right ctx-chevron"></i></button>`;
const subPanelsHTML =
MENU_GROUPS.map(g =>
`<div class="ctx-sub-panel" data-for="${g.id}">${g.items.map(buildItemHTML).join("")}</div>`
).join("") +
`<div class="ctx-sub-panel" data-for="scripts"><div id="script-menu" style="display:flex;flex-direction:column;gap:1px"></div></div>` +
`<div class="ctx-sub-panel" data-for="plugins"><div id="plugin-menu" style="display:flex;flex-direction:column;gap:1px"></div></div>`;
const menu = document.createElement("div");
@@ -7,6 +7,7 @@ import { getConfig } from "../config";
export const BUILTIN_TRUSTED_KEYS: string[] = [
"c0f7b94f9678425d08d2408fd92494c419a5ea6819ee4dbcebc41d86c22c61fa",
"f584e6b7f9c0e1dc4037e3f667918707a95a86d075bfc49bd4eebf0571eb0ff6",
];
export async function computeContentDigest(zip: InstanceType<typeof AdmZip>): Promise<string> {
@@ -532,28 +532,72 @@ export async function handleFileDownloadRoutes(
return new Response("Forbidden", { status: 403 });
}
const rangeHeader = req.headers.get("range") || req.headers.get("Range") || "";
logger.debug("[filebrowser] http upload pull", {
pullId,
clientId: pull.clientId,
path: pull.path,
bytes: pull.size,
range: rangeHeader,
ip: server.requestIP(req)?.address || "unknown",
});
uploadPulls.delete(pullId);
clearTimeout(pull.timeout);
const headers = {
const fileName = deps.sanitizeOutputName(path.basename(pull.fileName) || "upload.bin");
const baseHeaders = {
...deps.secureHeaders("application/octet-stream"),
"Content-Disposition": `attachment; filename="${deps.sanitizeOutputName(path.basename(pull.fileName) || "upload.bin")}"`,
"Content-Disposition": `attachment; filename="${fileName}"`,
"Cache-Control": "no-store, private",
"Content-Length": String(pull.size),
"Accept-Ranges": "bytes",
};
const body = pull.deleteFile
? streamFileAndDelete(pull.tmpPath)
: Bun.file(pull.tmpPath).stream();
return new Response(body, { headers });
if (pull.deleteFile) {
uploadPulls.delete(pullId);
clearTimeout(pull.timeout);
return new Response(streamFileAndDelete(pull.tmpPath), {
headers: { ...baseHeaders, "Content-Length": String(pull.size) },
});
}
if (rangeHeader) {
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader.trim());
if (!match) {
return new Response("Range Not Satisfiable", {
status: 416,
headers: { ...baseHeaders, "Content-Range": `bytes */${pull.size}` },
});
}
const start = Number(match[1]);
const end = match[2] === "" ? pull.size - 1 : Number(match[2]);
if (
!Number.isFinite(start) ||
!Number.isFinite(end) ||
start < 0 ||
end >= pull.size ||
start > end
) {
return new Response("Range Not Satisfiable", {
status: 416,
headers: { ...baseHeaders, "Content-Range": `bytes */${pull.size}` },
});
}
const length = end - start + 1;
const sliceStream = (Bun.file(pull.tmpPath) as any)
.slice(start, end + 1)
.stream();
return new Response(sliceStream, {
status: 206,
headers: {
...baseHeaders,
"Content-Range": `bytes ${start}-${end}/${pull.size}`,
"Content-Length": String(length),
},
});
}
return new Response(Bun.file(pull.tmpPath).stream(), {
headers: { ...baseHeaders, "Content-Length": String(pull.size) },
});
}
if (!url.pathname.startsWith("/api/file/download")) {