FIX - HUB ADMIN: stabilize NAS persistence
This commit is contained in:
@@ -65,6 +65,16 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
const serverStorageRoot = resolveStorageRoot(projectRoot);
|
||||
const legacyPublicDataPath = join(projectRoot, "public", "storage", "launcher-data.json");
|
||||
const dataPath = join(serverStorageRoot, "launcher-data.json");
|
||||
let mutationQueue = Promise.resolve();
|
||||
|
||||
function enqueueMutation(operation) {
|
||||
const nextOperation = mutationQueue.catch(() => {}).then(operation);
|
||||
mutationQueue = nextOperation.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return nextOperation;
|
||||
}
|
||||
|
||||
function readData() {
|
||||
const readablePath = existsSync(dataPath) ? dataPath : legacyPublicDataPath;
|
||||
@@ -351,33 +361,35 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
}
|
||||
|
||||
async function updateSettings(payload, identity) {
|
||||
const data = readData();
|
||||
const actor = resolveActor(data, identity);
|
||||
const patch = typeof payload === "object" && payload !== null ? payload : {};
|
||||
const settings = normalizeSettings({
|
||||
...data.settings,
|
||||
...patch,
|
||||
brand: {
|
||||
...(data.settings?.brand ?? {}),
|
||||
...(patch.brand ?? {}),
|
||||
},
|
||||
taskManager: {
|
||||
...(data.settings?.taskManager ?? {}),
|
||||
...(patch.taskManager ?? {}),
|
||||
},
|
||||
});
|
||||
return enqueueMutation(async () => {
|
||||
const data = readData();
|
||||
const actor = resolveActor(data, identity);
|
||||
const patch = typeof payload === "object" && payload !== null ? payload : {};
|
||||
const settings = normalizeSettings({
|
||||
...data.settings,
|
||||
...patch,
|
||||
brand: {
|
||||
...(data.settings?.brand ?? {}),
|
||||
...(patch.brand ?? {}),
|
||||
},
|
||||
taskManager: {
|
||||
...(data.settings?.taskManager ?? {}),
|
||||
...(patch.taskManager ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
data.settings = settings;
|
||||
addAuditEvent(data, actor, {
|
||||
action: "Обновлены системные настройки",
|
||||
objectType: "settings",
|
||||
objectName: "Brand settings",
|
||||
result: "success",
|
||||
details: `Logo link: ${settings.brand.logoLinkUrl}; Tasker workspace policy: ${settings.taskManager.workspaceCreationPolicy}`,
|
||||
});
|
||||
data.settings = settings;
|
||||
addAuditEvent(data, actor, {
|
||||
action: "Обновлены системные настройки",
|
||||
objectType: "settings",
|
||||
objectName: "Brand settings",
|
||||
result: "success",
|
||||
details: `Logo link: ${settings.brand.logoLinkUrl}; Tasker workspace policy: ${settings.taskManager.workspaceCreationPolicy}`,
|
||||
});
|
||||
|
||||
await writeData(data);
|
||||
return { settings, data };
|
||||
await writeData(data);
|
||||
return { settings, data };
|
||||
});
|
||||
}
|
||||
|
||||
async function updateUserProfile(userId, payload, identity) {
|
||||
@@ -1540,24 +1552,26 @@ export function createControlPlaneStore({ projectRoot }) {
|
||||
}
|
||||
|
||||
async function updateService(serviceId, payload, identity) {
|
||||
const data = readData();
|
||||
const actor = resolveActor(data, identity);
|
||||
const service = findById(data.services, serviceId, "service");
|
||||
return enqueueMutation(async () => {
|
||||
const data = readData();
|
||||
const actor = resolveActor(data, identity);
|
||||
const service = findById(data.services, serviceId, "service");
|
||||
|
||||
Object.assign(service, sanitizeServicePatch(payload, service));
|
||||
Object.assign(service, syncServiceLaunchLink(service));
|
||||
service.updatedAt = isoNow();
|
||||
Object.assign(service, sanitizeServicePatch(payload, service));
|
||||
Object.assign(service, syncServiceLaunchLink(service));
|
||||
service.updatedAt = isoNow();
|
||||
|
||||
addAuditEvent(data, actor, {
|
||||
action: "Обновлён сервис",
|
||||
objectType: "service",
|
||||
objectName: service.title,
|
||||
result: "success",
|
||||
addAuditEvent(data, actor, {
|
||||
action: "Обновлён сервис",
|
||||
objectType: "service",
|
||||
objectName: service.title,
|
||||
result: "success",
|
||||
});
|
||||
markPendingSync(data, service, "service");
|
||||
|
||||
await writeData(data);
|
||||
return { service, data };
|
||||
});
|
||||
markPendingSync(data, service, "service");
|
||||
|
||||
await writeData(data);
|
||||
return { service, data };
|
||||
}
|
||||
|
||||
async function reorderServices(payload, identity) {
|
||||
|
||||
+40
-11
@@ -5,7 +5,6 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, extname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||
import { createAuthentikSyncClient, resolveRequiredGroups } from "./authentik-sync.mjs";
|
||||
import { createControlPlaneStore } from "./control-plane-store.mjs";
|
||||
@@ -1508,19 +1507,49 @@ app.get("/storage/launcher-data.json", (_req, res) => {
|
||||
res.status(404).json({ error: "not_found" });
|
||||
});
|
||||
|
||||
const vite = await createViteServer({
|
||||
root: projectRoot,
|
||||
appType: "spa",
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: { server: httpServer },
|
||||
},
|
||||
});
|
||||
let fixFrontendStacktrace = () => {};
|
||||
|
||||
app.use(vite.middlewares);
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const distRoot = join(projectRoot, "dist");
|
||||
const indexHtmlPath = join(distRoot, "index.html");
|
||||
|
||||
if (!existsSync(indexHtmlPath)) {
|
||||
throw new Error("Launcher production build is missing. Run npm run build before starting the server.");
|
||||
}
|
||||
|
||||
app.use(express.static(distRoot, { index: false }));
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
|
||||
if (!accept.includes("text/html")) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
setNoStore(res);
|
||||
res.sendFile(indexHtmlPath);
|
||||
});
|
||||
} else {
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const vite = await createViteServer({
|
||||
root: projectRoot,
|
||||
appType: "spa",
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: { server: httpServer },
|
||||
},
|
||||
});
|
||||
|
||||
fixFrontendStacktrace = (error) => vite.ssrFixStacktrace(error);
|
||||
app.use(vite.middlewares);
|
||||
}
|
||||
|
||||
app.use((error, _req, res, _next) => {
|
||||
vite.ssrFixStacktrace(error);
|
||||
fixFrontendStacktrace(error);
|
||||
const message = error instanceof Error ? error.message : "Unexpected server error";
|
||||
res.status(500).json({ error: message });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user