FIX - HUB ADMIN: stabilize NAS persistence

This commit is contained in:
DCCONSTRUCTIONS
2026-05-14 13:15:35 +03:00
parent 784195f747
commit 5898b94875
7 changed files with 256 additions and 101 deletions
+54 -40
View File
@@ -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
View File
@@ -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 });
});