fix builder issues as well + fix approved clients not showing up in dashboard if it's an operator
This commit is contained in:
@@ -42,7 +42,7 @@ function getDefaultServerUrlPlaceholder(isRawList) {
|
||||
if (isRawList) {
|
||||
return `${isHttps ? "https" : "http"}://${host}/list.txt`;
|
||||
}
|
||||
return `${isHttps ? "wss" : "ws"}://${host}`;
|
||||
return host;
|
||||
}
|
||||
|
||||
function updateServerUrlPlaceholder() {
|
||||
@@ -1124,90 +1124,209 @@ async function startBuild(config) {
|
||||
}
|
||||
}
|
||||
|
||||
async function streamBuildOutput(buildId, config = {}) {
|
||||
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 = "";
|
||||
|
||||
async function checkBuildInfo(buildId) {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const res = await fetch(`/api/build/${buildId}/info`, { credentials: "include" });
|
||||
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");
|
||||
buffer = lines.pop();
|
||||
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 while reconnecting.\n", "success");
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
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);
|
||||
|
||||
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();
|
||||
return;
|
||||
} else if (data.type === "error") {
|
||||
addBuildOutput(`\nERROR: ${data.error}\n`, "error");
|
||||
if (pendingUpdateAll) {
|
||||
pendingUpdateAll = false;
|
||||
await pushUpdateToAllClients(buildData.id, pendingUpdateHideWindow);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
|
||||
<div>
|
||||
<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" />
|
||||
<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" />
|
||||
|
||||
@@ -16,16 +16,22 @@ import { logAudit, AuditAction } from "../../auditLog";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { setOnlineState } from "../../db";
|
||||
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;
|
||||
const currentScope = getUserClientAccessScope(userId);
|
||||
|
||||
function grantBuildOwnerAccess(clientId: string): void {
|
||||
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") {
|
||||
setUserClientAccessScope(userId, "allowlist");
|
||||
setUserClientAccessScope(ownership.builtByUserId, "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);
|
||||
|
||||
grantClientAccess(user.userId, user.role, clientId);
|
||||
grantBuildOwnerAccess(clientId);
|
||||
|
||||
_postApproveHook?.(clientId);
|
||||
|
||||
@@ -275,7 +281,7 @@ export async function handleEnrollmentRoutes(
|
||||
if (ok) {
|
||||
updated++;
|
||||
if (action === "approve") {
|
||||
grantClientAccess(user.userId, user.role, id);
|
||||
grantBuildOwnerAccess(id);
|
||||
_postApproveHook?.(id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user