fix builder issues as well + fix approved clients not showing up in dashboard if it's an operator

This commit is contained in:
KingKDot
2026-04-14 10:35:33 -05:00
parent 303b7a43ba
commit a0e4a88790
3 changed files with 208 additions and 83 deletions
+193 -74
View File
@@ -42,7 +42,7 @@ function getDefaultServerUrlPlaceholder(isRawList) {
if (isRawList) { if (isRawList) {
return `${isHttps ? "https" : "http"}://${host}/list.txt`; return `${isHttps ? "https" : "http"}://${host}/list.txt`;
} }
return `${isHttps ? "wss" : "ws"}://${host}`; return host;
} }
function updateServerUrlPlaceholder() { function updateServerUrlPlaceholder() {
@@ -1124,90 +1124,209 @@ async function startBuild(config) {
} }
} }
async function streamBuildOutput(buildId, config = {}) { async function checkBuildInfo(buildId) {
const res = await fetch(`/api/build/${buildId}/stream`, {
credentials: "include",
});
if (!res.ok) {
throw new Error("Failed to connect to build stream");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try { try {
while (true) { const res = await fetch(`/api/build/${buildId}/info`, { credentials: "include" });
const { done, value } = await reader.read(); if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
if (done) break; async function streamBuildOutput(buildId, config = {}) {
const MAX_RECONNECT_ATTEMPTS = 10;
const RECONNECT_DELAY_MS = 2000;
let attempts = 0;
let completed = false;
buffer += decoder.decode(value, { stream: true }); while (!completed && attempts <= MAX_RECONNECT_ATTEMPTS) {
if (attempts > 0) {
addBuildOutput(`\nReconnecting to build stream (attempt ${attempts}/${MAX_RECONNECT_ATTEMPTS})...\n`, "warn");
buildStatusText.textContent = "Reconnecting to build stream...";
await new Promise((r) => setTimeout(r, RECONNECT_DELAY_MS));
const lines = buffer.split("\n"); const info = await checkBuildInfo(buildId);
buffer = lines.pop(); if (info && (info.status === "completed" || info.status === "success")) {
buildStatusText.textContent = "Build completed successfully!";
buildStatus.querySelector("div").className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60";
buildStatus.querySelector("i").className = "fa-solid fa-circle-check";
addBuildOutput("\nBuild completed while reconnecting.\n", "success");
for (const line of lines) { if (info.files && info.files.length > 0) {
if (!line.trim()) continue; const buildData = {
id: info.id || buildId,
status: "success",
startTime: info.startTime || Date.now(),
expiresAt: info.expiresAt,
files: info.files,
};
saveBuildToStorage(buildData.id, buildData);
buildResults.classList.remove("hidden");
displayBuild(buildData);
if (line.startsWith("data: ")) { if (pendingUpdateAll) {
const data = JSON.parse(line.substring(6)); pendingUpdateAll = false;
await pushUpdateToAllClients(buildData.id, pendingUpdateHideWindow);
if (data.type === "output") {
addBuildOutput(data.text, data.level || "info");
} else if (data.type === "status") {
buildStatusText.textContent = data.text;
} else if (data.type === "complete") {
buildStatusText.textContent = data.success
? "Build completed successfully!"
: "Build failed";
buildStatus.querySelector("div").className = data.success
? "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"
: "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60";
buildStatus.querySelector("i").className = data.success
? "fa-solid fa-circle-check"
: "fa-solid fa-circle-xmark";
if (!data.success && !config.disableCgo) {
addBuildOutput(
"Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n",
"warn",
);
}
if (data.success && data.files) {
const buildData = {
id: data.buildId,
status: "success",
startTime: Date.now(),
expiresAt: data.expiresAt,
files: data.files,
};
saveBuildToStorage(data.buildId, buildData);
buildResults.classList.remove("hidden");
displayBuild(buildData);
// Auto-push update if "Build & Update All" was used
if (pendingUpdateAll) {
pendingUpdateAll = false;
await pushUpdateToAllClients(data.buildId, pendingUpdateHideWindow);
}
}
reader.cancel();
return;
} else if (data.type === "error") {
addBuildOutput(`\nERROR: ${data.error}\n`, "error");
} }
} }
return;
} else if (info && info.status === "failed") {
buildStatusText.textContent = "Build failed";
buildStatus.querySelector("div").className =
"flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60";
buildStatus.querySelector("i").className = "fa-solid fa-circle-xmark";
addBuildOutput("\nBuild failed while reconnecting.\n", "error");
return;
} }
// status is still "running", reconnect to stream
}
buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight; let res;
try {
res = await fetch(`/api/build/${buildId}/stream`, { credentials: "include" });
} catch (err) {
attempts++;
continue;
}
if (!res.ok) {
if (attempts > 0) {
const info = await checkBuildInfo(buildId);
if (info && (info.status === "completed" || info.status === "success")) {
buildStatusText.textContent = "Build completed successfully!";
buildStatus.querySelector("div").className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60";
buildStatus.querySelector("i").className = "fa-solid fa-circle-check";
if (info.files && info.files.length > 0) {
const buildData = {
id: info.id || buildId,
status: "success",
startTime: info.startTime || Date.now(),
expiresAt: info.expiresAt,
files: info.files,
};
saveBuildToStorage(buildData.id, buildData);
buildResults.classList.remove("hidden");
displayBuild(buildData);
}
return;
}
}
throw new Error("Failed to connect to build stream");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
if (line.startsWith("data: ")) {
const data = JSON.parse(line.substring(6));
if (data.type === "output") {
addBuildOutput(data.text, data.level || "info");
} else if (data.type === "status") {
buildStatusText.textContent = data.text;
} else if (data.type === "complete") {
buildStatusText.textContent = data.success
? "Build completed successfully!"
: "Build failed";
buildStatus.querySelector("div").className = data.success
? "flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60"
: "flex items-center gap-2 p-3 rounded-lg bg-red-900/40 border border-red-700/60";
buildStatus.querySelector("i").className = data.success
? "fa-solid fa-circle-check"
: "fa-solid fa-circle-xmark";
if (!data.success && !config.disableCgo) {
addBuildOutput(
"Hint: This build used CGO. If it keeps failing, try enabling the 'Disable CGO' option and build again.\n",
"warn",
);
}
if (data.success && data.files) {
const buildData = {
id: data.buildId,
status: "success",
startTime: Date.now(),
expiresAt: data.expiresAt,
files: data.files,
};
saveBuildToStorage(data.buildId, buildData);
buildResults.classList.remove("hidden");
displayBuild(buildData);
// Auto-push update if "Build & Update All" was used
if (pendingUpdateAll) {
pendingUpdateAll = false;
await pushUpdateToAllClients(data.buildId, pendingUpdateHideWindow);
}
}
reader.cancel();
completed = true;
return;
} else if (data.type === "error") {
addBuildOutput(`\nERROR: ${data.error}\n`, "error");
}
}
}
buildOutputContainer.scrollTop = buildOutputContainer.scrollHeight;
}
// Stream ended cleanly without a "complete" event — build may still be running
completed = true;
} catch (streamErr) {
// Network error — try to reconnect
try { reader.releaseLock(); } catch {}
attempts++;
continue;
} finally {
try { reader.releaseLock(); } catch {}
}
}
if (!completed && attempts > MAX_RECONNECT_ATTEMPTS) {
const info = await checkBuildInfo(buildId);
if (info && (info.status === "completed" || info.status === "success")) {
buildStatusText.textContent = "Build completed successfully!";
buildStatus.querySelector("div").className =
"flex items-center gap-2 p-3 rounded-lg bg-green-900/40 border border-green-700/60";
buildStatus.querySelector("i").className = "fa-solid fa-circle-check";
addBuildOutput("\nBuild completed (recovered after stream loss).\n", "success");
if (info.files && info.files.length > 0) {
const buildData = {
id: info.id || buildId,
status: "success",
startTime: info.startTime || Date.now(),
expiresAt: info.expiresAt,
files: info.files,
};
saveBuildToStorage(buildData.id, buildData);
buildResults.classList.remove("hidden");
displayBuild(buildData);
}
} else {
addBuildOutput("\nLost connection to build stream. The build may still be running on the server.\n", "warn");
addBuildOutput("Refresh the page to check build results.\n", "warn");
} }
} finally {
reader.releaseLock();
} }
} }
+1 -1
View File
@@ -150,7 +150,7 @@
<div> <div>
<label class="block text-sm font-medium text-slate-300 mb-2" for="server-url">Server URL</label> <label class="block text-sm font-medium text-slate-300 mb-2" for="server-url">Server URL</label>
<input type="text" id="server-url" name="server-url" placeholder="wss://your-server:5173" <input type="text" id="server-url" name="server-url" placeholder="your-server:5173"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" /> class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors" />
<label class="mt-2 flex items-center gap-2 text-xs text-slate-400"> <label class="mt-2 flex items-center gap-2 text-xs text-slate-400">
<input type="checkbox" id="raw-server-list" name="raw-server-list" class="w-4 h-4" /> <input type="checkbox" id="raw-server-list" name="raw-server-list" class="w-4 h-4" />
@@ -16,16 +16,22 @@ import { logAudit, AuditAction } from "../../auditLog";
import * as clientManager from "../../clientManager"; import * as clientManager from "../../clientManager";
import { setOnlineState } from "../../db"; import { setOnlineState } from "../../db";
import { requirePermission } from "../../rbac"; import { requirePermission } from "../../rbac";
import { getUserClientAccessScope, setUserClientAccessScope, setUserClientAccessRule } from "../../users"; import { getUserById, getUserClientAccessScope, setUserClientAccessScope, setUserClientAccessRule } from "../../users";
function grantClientAccess(userId: number, role: string, clientId: string): void {
if (role === "admin") return; function grantBuildOwnerAccess(clientId: string): void {
const currentScope = getUserClientAccessScope(userId); const ownership = getClientBuildOwnership(clientId);
if (!ownership || ownership.builtByUserId == null) return;
const owner = getUserById(ownership.builtByUserId);
if (!owner || owner.role === "admin") return;
const currentScope = getUserClientAccessScope(ownership.builtByUserId);
if (currentScope === "none") { if (currentScope === "none") {
setUserClientAccessScope(userId, "allowlist"); setUserClientAccessScope(ownership.builtByUserId, "allowlist");
} }
if (currentScope === "none" || currentScope === "allowlist") { if (currentScope === "none" || currentScope === "allowlist") {
setUserClientAccessRule(userId, clientId, "allow"); setUserClientAccessRule(ownership.builtByUserId, clientId, "allow");
} }
} }
@@ -130,7 +136,7 @@ export async function handleEnrollmentRoutes(
setClientEnrollmentStatus(clientId, "approved", user.username); setClientEnrollmentStatus(clientId, "approved", user.username);
grantClientAccess(user.userId, user.role, clientId); grantBuildOwnerAccess(clientId);
_postApproveHook?.(clientId); _postApproveHook?.(clientId);
@@ -275,7 +281,7 @@ export async function handleEnrollmentRoutes(
if (ok) { if (ok) {
updated++; updated++;
if (action === "approve") { if (action === "approve") {
grantClientAccess(user.userId, user.role, id); grantBuildOwnerAccess(id);
_postApproveHook?.(id); _postApproveHook?.(id);
} }
} }