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) {
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user