beam: stabilize startup model loading
This commit is contained in:
parent
6e1f5a7f08
commit
4ecebd5e79
|
|
@ -939,6 +939,40 @@ const log = (text, isError = false) => {
|
||||||
if (isError) console.error(text);
|
if (isError) console.error(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reportViewerEvent = (event, data = {}, level = "info") => {
|
||||||
|
const payload = {
|
||||||
|
event,
|
||||||
|
level,
|
||||||
|
data,
|
||||||
|
href: window.location.href,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const logger = level === "error" ? console.error : console.log;
|
||||||
|
logger(`[beam-viewer] ${event}`, data);
|
||||||
|
fetch("/api/client-log", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
keepalive: true,
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("error", (event) => {
|
||||||
|
reportViewerEvent("window-error", {
|
||||||
|
message: event.message,
|
||||||
|
filename: event.filename,
|
||||||
|
lineno: event.lineno,
|
||||||
|
colno: event.colno,
|
||||||
|
}, "error");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
|
reportViewerEvent("window-unhandled-rejection", {
|
||||||
|
reason: event.reason?.message || String(event.reason),
|
||||||
|
stack: event.reason?.stack,
|
||||||
|
}, "error");
|
||||||
|
});
|
||||||
|
|
||||||
const setStatus = (text) => {
|
const setStatus = (text) => {
|
||||||
statusText.textContent = text;
|
statusText.textContent = text;
|
||||||
statusText.style.color = "";
|
statusText.style.color = "";
|
||||||
|
|
@ -964,6 +998,51 @@ const setLoading = (flag) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const probeWebGLContext = (canvas, contextAttr = {}) => {
|
||||||
|
if (!canvas) return null;
|
||||||
|
const contextNames = ["webgl2", "experimental-webgl", "webgl"];
|
||||||
|
for (const contextName of contextNames) {
|
||||||
|
try {
|
||||||
|
const gl = canvas.getContext(contextName, contextAttr);
|
||||||
|
if (gl) return { contextName, gl };
|
||||||
|
} catch (_error) {
|
||||||
|
// Try next context type.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForViewerCanvasReady = async (contextAttr = {}) => {
|
||||||
|
for (let attempt = 1; attempt <= 30; attempt++) {
|
||||||
|
const rect = mainCanvas?.getBoundingClientRect?.();
|
||||||
|
const hasSize = !!rect && rect.width >= 32 && rect.height >= 32;
|
||||||
|
const webglProbe = hasSize ? probeWebGLContext(mainCanvas, contextAttr) : null;
|
||||||
|
if (hasSize && webglProbe?.gl) {
|
||||||
|
reportViewerEvent("webgl-ready", {
|
||||||
|
attempt,
|
||||||
|
contextName: webglProbe.contextName,
|
||||||
|
height: Math.round(rect.height),
|
||||||
|
width: Math.round(rect.width),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attempt === 1 || attempt % 10 === 0) {
|
||||||
|
reportViewerEvent("webgl-wait", {
|
||||||
|
attempt,
|
||||||
|
hasCanvas: !!mainCanvas,
|
||||||
|
height: rect ? Math.round(rect.height) : 0,
|
||||||
|
hasSize,
|
||||||
|
width: rect ? Math.round(rect.width) : 0,
|
||||||
|
webgl: !!webglProbe?.gl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await delay(150);
|
||||||
|
}
|
||||||
|
throw new Error("WebGL-контекст недоступен: браузер не выдал canvas context для BIM Viewer");
|
||||||
|
};
|
||||||
|
|
||||||
const updateNavCubeVisibility = () => {
|
const updateNavCubeVisibility = () => {
|
||||||
if (!navCubeCanvas) return;
|
if (!navCubeCanvas) return;
|
||||||
navCubeCanvas.style.display = activeModels.length ? "block" : "none";
|
navCubeCanvas.style.display = activeModels.length ? "block" : "none";
|
||||||
|
|
@ -2171,6 +2250,19 @@ const fetchJSON = async (url, options = {}) => {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchJSONWithTimeout = async (url, options = {}, timeoutMs = 5000) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
return await fetchJSON(url, {
|
||||||
|
...options,
|
||||||
|
signal: options.signal || controller.signal,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const uploadBuffer = async (bytes, filename) => {
|
const uploadBuffer = async (bytes, filename) => {
|
||||||
const safeName = filename || "upload.bin";
|
const safeName = filename || "upload.bin";
|
||||||
const params = new URLSearchParams({ filename: safeName });
|
const params = new URLSearchParams({ filename: safeName });
|
||||||
|
|
@ -2519,7 +2611,13 @@ const fetchModelSettings = async (src) => {
|
||||||
const normalized = normalizeModelSettingsSrc(src);
|
const normalized = normalizeModelSettingsSrc(src);
|
||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
const params = new URLSearchParams({ src: normalized });
|
const params = new URLSearchParams({ src: normalized });
|
||||||
return fetchJSON(`${MODEL_SETTINGS_API}?${params.toString()}`);
|
reportViewerEvent("model-settings-request", { src: normalized });
|
||||||
|
const data = await fetchJSONWithTimeout(`${MODEL_SETTINGS_API}?${params.toString()}`, {}, 2500);
|
||||||
|
reportViewerEvent("model-settings-response", {
|
||||||
|
src: data?.sourceSrc || normalized,
|
||||||
|
hasViewerSettings: !!data?.viewerSettings,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveCurrentModelSettings = async () => {
|
const saveCurrentModelSettings = async () => {
|
||||||
|
|
@ -2691,9 +2789,13 @@ const initViewer = async () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewerContextAttr = {};
|
||||||
|
await waitForViewerCanvasReady(viewerContextAttr);
|
||||||
|
|
||||||
viewer = new Viewer({
|
viewer = new Viewer({
|
||||||
canvasId: "viewerCanvas",
|
canvasId: "viewerCanvas",
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
contextAttr: viewerContextAttr,
|
||||||
dtxEnabled: true,
|
dtxEnabled: true,
|
||||||
colorTextureEnabled: true,
|
colorTextureEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
@ -3012,12 +3114,14 @@ const initViewer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadModel = (options) => {
|
const loadModel = (options) => {
|
||||||
|
const debugOptions = options || {};
|
||||||
try {
|
try {
|
||||||
const { type, url, name = "", replace = true, id: forcedId, meta, edges } = options;
|
const { type, url, name = "", replace = true, id: forcedId, meta, edges } = options;
|
||||||
const modelSettings = meta?.viewerSettings;
|
const modelSettings = meta?.viewerSettings;
|
||||||
const normalizedType = normalizeType(type, typeof url === "string" ? url : "");
|
const normalizedType = normalizeType(type, typeof url === "string" ? url : "");
|
||||||
const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : ""));
|
const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : ""));
|
||||||
if (!inferredType || !acceptByType[inferredType]) {
|
if (!inferredType || !acceptByType[inferredType]) {
|
||||||
|
reportViewerEvent("load-model-unsupported", { type, url, name, normalizedType, inferredType }, "error");
|
||||||
showError("Формат файла не поддерживается. Выберите .xkt, .glb, .gltf, .bim, .las, .laz, .obj, .stl");
|
showError("Формат файла не поддерживается. Выберите .xkt, .glb, .gltf, .bim, .las, .laz, .obj, .stl");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -3035,6 +3139,14 @@ const initViewer = async () => {
|
||||||
const isBlob = typeof url === "string" && url.startsWith("blob:");
|
const isBlob = typeof url === "string" && url.startsWith("blob:");
|
||||||
const isMemory = typeof url === "string" && url.startsWith("memory:");
|
const isMemory = typeof url === "string" && url.startsWith("memory:");
|
||||||
log(`loader: ${inferredType}, blob=${isBlob}, memory=${isMemory}, url=${url}`, false);
|
log(`loader: ${inferredType}, blob=${isBlob}, memory=${isMemory}, url=${url}`, false);
|
||||||
|
reportViewerEvent("load-model-start", {
|
||||||
|
inferredType,
|
||||||
|
isBlob,
|
||||||
|
isMemory,
|
||||||
|
name,
|
||||||
|
replace,
|
||||||
|
url,
|
||||||
|
});
|
||||||
const xktDataSource = inferredType === "xkt"
|
const xktDataSource = inferredType === "xkt"
|
||||||
? (isBlob || isMemory ? createMemoryDataSource() : new sdk.XKTDefaultDataSource({ cacheBuster: !isBlob }))
|
? (isBlob || isMemory ? createMemoryDataSource() : new sdk.XKTDefaultDataSource({ cacheBuster: !isBlob }))
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -3073,6 +3185,13 @@ const initViewer = async () => {
|
||||||
modelTypes[id] = inferredType;
|
modelTypes[id] = inferredType;
|
||||||
|
|
||||||
currentModel.on("loaded", () => {
|
currentModel.on("loaded", () => {
|
||||||
|
reportViewerEvent("model-loaded", {
|
||||||
|
id,
|
||||||
|
inferredType,
|
||||||
|
name,
|
||||||
|
objectCount: Object.keys(viewer.scene.objects || {}).length,
|
||||||
|
src: srcToLoad,
|
||||||
|
});
|
||||||
setTimeout(() => applyStoredTransformsToModel(id), 0);
|
setTimeout(() => applyStoredTransformsToModel(id), 0);
|
||||||
setTimeout(() => applyStoredTransformsToModel(id), 60);
|
setTimeout(() => applyStoredTransformsToModel(id), 60);
|
||||||
const hasModelCamera = !!modelSettings?.viewerState?.camera;
|
const hasModelCamera = !!modelSettings?.viewerState?.camera;
|
||||||
|
|
@ -3111,6 +3230,13 @@ const initViewer = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
currentModel.on("error", (err) => {
|
currentModel.on("error", (err) => {
|
||||||
|
reportViewerEvent("model-error", {
|
||||||
|
id,
|
||||||
|
inferredType,
|
||||||
|
name,
|
||||||
|
error: err?.message || String(err),
|
||||||
|
src: srcToLoad,
|
||||||
|
}, "error");
|
||||||
showError(`Ошибка загрузки: ${err}`);
|
showError(`Ошибка загрузки: ${err}`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
updateNavCubeVisibility();
|
updateNavCubeVisibility();
|
||||||
|
|
@ -3132,6 +3258,12 @@ const initViewer = async () => {
|
||||||
return currentModel;
|
return currentModel;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
reportViewerEvent("load-model-exception", {
|
||||||
|
type: debugOptions.type,
|
||||||
|
url: debugOptions.url,
|
||||||
|
name: debugOptions.name,
|
||||||
|
error: e?.message || String(e),
|
||||||
|
}, "error");
|
||||||
showError(e.message || "Ошибка загрузки");
|
showError(e.message || "Ошибка загрузки");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -3151,28 +3283,33 @@ const initViewer = async () => {
|
||||||
const loadStartupModelFromQuery = async () => {
|
const loadStartupModelFromQuery = async () => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const src = params.get("url") || params.get("src");
|
const src = params.get("url") || params.get("src");
|
||||||
if (!src) return;
|
reportViewerEvent("startup-query", {
|
||||||
|
search: window.location.search,
|
||||||
|
hasUrl: params.has("url"),
|
||||||
|
hasSrc: params.has("src"),
|
||||||
|
src,
|
||||||
|
typeParam: params.get("type"),
|
||||||
|
nameParam: params.get("name"),
|
||||||
|
});
|
||||||
|
if (!src) {
|
||||||
|
reportViewerEvent("startup-no-src", { search: window.location.search }, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const name = params.get("name") || src.split("/").pop() || "model";
|
const name = params.get("name") || src.split("/").pop() || "model";
|
||||||
const type = normalizeType(params.get("type"), src) || normalizeType(guessTypeFromName(name));
|
const type = normalizeType(params.get("type"), src) || normalizeType(guessTypeFromName(name));
|
||||||
if (!type) {
|
if (!type) {
|
||||||
|
reportViewerEvent("startup-type-error", { src, name, typeParam: params.get("type") }, "error");
|
||||||
showError("Не удалось определить формат модели из URL");
|
showError("Не удалось определить формат модели из URL");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let settingsSrc = params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
dropZone?.classList.add("hidden");
|
||||||
let modelSettings = null;
|
setStatus(`Подготовка: ${name}...`);
|
||||||
try {
|
setLoading(true);
|
||||||
const settingsResponse = await fetchModelSettings(settingsSrc);
|
|
||||||
modelSettings = settingsResponse?.viewerSettings || null;
|
|
||||||
settingsSrc = settingsResponse?.sourceSrc || settingsSrc;
|
|
||||||
if (modelSettings) {
|
|
||||||
applyModelViewerSettings(modelSettings, { applyCamera: false });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("model settings unavailable", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let settingsSrc = params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src;
|
||||||
|
const applyQueryViewerOverrides = () => {
|
||||||
if (params.has("displayMode")) {
|
if (params.has("displayMode")) {
|
||||||
activeDisplayMode = params.get("displayMode") || "source";
|
activeDisplayMode = params.get("displayMode") || "source";
|
||||||
setDisplayModeControlState();
|
setDisplayModeControlState();
|
||||||
|
|
@ -3191,38 +3328,86 @@ const initViewer = async () => {
|
||||||
if (params.has("zoomSpeed")) {
|
if (params.has("zoomSpeed")) {
|
||||||
setZoomSpeed(params.get("zoomSpeed"));
|
setZoomSpeed(params.get("zoomSpeed"));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
applyQueryViewerOverrides();
|
||||||
|
|
||||||
if (modelSettings?.viewerState) {
|
const startupMeta = {
|
||||||
modelSettings = {
|
label: name,
|
||||||
...modelSettings,
|
source: "query",
|
||||||
|
settingsSrc: normalizeModelSettingsSrc(settingsSrc) || normalizeModelSettingsSrc(src),
|
||||||
|
artifactSrc: normalizeModelSettingsSrc(src),
|
||||||
|
viewerSettings: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFetchedStartupSettings = (settingsResponse) => {
|
||||||
|
const modelSettings = settingsResponse?.viewerSettings || null;
|
||||||
|
settingsSrc = settingsResponse?.sourceSrc || settingsSrc;
|
||||||
|
reportViewerEvent("startup-settings-loaded", {
|
||||||
|
settingsSrc,
|
||||||
|
hasViewerSettings: !!modelSettings,
|
||||||
|
});
|
||||||
|
if (!modelSettings) return;
|
||||||
|
const normalizedSettingsSrc = normalizeModelSettingsSrc(settingsSrc) || startupMeta.settingsSrc;
|
||||||
|
startupMeta.settingsSrc = normalizedSettingsSrc;
|
||||||
|
startupMeta.viewerSettings = modelSettings;
|
||||||
|
const entry = activeModels.find((item) => item?.meta === startupMeta) || getPrimaryModelEntry();
|
||||||
|
if (entry) {
|
||||||
|
entry.meta = {
|
||||||
|
...(entry.meta || {}),
|
||||||
|
settingsSrc: normalizedSettingsSrc,
|
||||||
|
sourceSrc: normalizedSettingsSrc,
|
||||||
|
artifactSrc: startupMeta.artifactSrc,
|
||||||
|
viewerSettings: modelSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
applyModelViewerSettings(modelSettings, { applyCamera: true, applyDesign: true });
|
||||||
|
applyQueryViewerOverrides();
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchModelSettings(settingsSrc)
|
||||||
|
.then(applyFetchedStartupSettings)
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("model settings unavailable", err);
|
||||||
|
reportViewerEvent("startup-settings-unavailable", {
|
||||||
|
settingsSrc,
|
||||||
|
error: err?.message || String(err),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.has("edges")) {
|
||||||
|
startupMeta.viewerSettings = {
|
||||||
viewerState: {
|
viewerState: {
|
||||||
...modelSettings.viewerState,
|
edges: params.get("edges") !== "false"
|
||||||
displayMode: activeDisplayMode,
|
|
||||||
clayColor: activeClayColor,
|
|
||||||
lightingMode: activeLightingMode,
|
|
||||||
navigationAxis: activeNavigationAxis,
|
|
||||||
zoomSpeed: activeZoomSpeed,
|
|
||||||
edges: params.has("edges") ? params.get("edges") !== "false" : modelSettings.viewerState.edges
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportViewerEvent("startup-load-model", {
|
||||||
|
src,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
settingsSrc,
|
||||||
|
hasViewerSettings: false,
|
||||||
|
});
|
||||||
loadModel({
|
loadModel({
|
||||||
type,
|
type,
|
||||||
url: src,
|
url: src,
|
||||||
name,
|
name,
|
||||||
replace: params.get("replace") !== "false",
|
replace: params.get("replace") !== "false",
|
||||||
edges: params.has("edges") ? params.get("edges") !== "false" : undefined,
|
edges: params.has("edges") ? params.get("edges") !== "false" : undefined,
|
||||||
meta: {
|
meta: startupMeta,
|
||||||
label: name,
|
|
||||||
source: "query",
|
|
||||||
settingsSrc: normalizeModelSettingsSrc(settingsSrc) || normalizeModelSettingsSrc(src),
|
|
||||||
artifactSrc: normalizeModelSettingsSrc(src),
|
|
||||||
viewerSettings: modelSettings,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startupModelLoadPromise = loadStartupModelFromQuery().catch((err) => {
|
||||||
|
console.warn("startup model load failed", err);
|
||||||
|
reportViewerEvent("startup-load-failed", {
|
||||||
|
message: err?.message || String(err),
|
||||||
|
stack: err?.stack,
|
||||||
|
}, "error");
|
||||||
|
showError(err.message || "Не удалось загрузить модель из URL");
|
||||||
|
});
|
||||||
|
|
||||||
const loadProjectSnapshot = async (project) => {
|
const loadProjectSnapshot = async (project) => {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error("Нет данных проекта");
|
throw new Error("Нет данных проекта");
|
||||||
|
|
@ -3969,14 +4154,17 @@ const loadProjectSnapshot = async (project) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStartupModelFromQuery().catch((err) => {
|
void startupModelLoadPromise;
|
||||||
console.warn("startup model load failed", err);
|
|
||||||
showError(err.message || "Не удалось загрузить модель из URL");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
initViewer();
|
initViewer().catch((err) => {
|
||||||
|
reportViewerEvent("init-viewer-error", {
|
||||||
|
message: err?.message || String(err),
|
||||||
|
stack: err?.stack,
|
||||||
|
}, "error");
|
||||||
|
showError(err?.message || "Не удалось инициализировать viewer");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// нижняя тулбар
|
// нижняя тулбар
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,11 @@ const sendText = (res, status, message) => {
|
||||||
res.end(message);
|
res.end(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const truncateLogValue = (value, maxLength = 2400) => {
|
||||||
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||||
|
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
||||||
|
};
|
||||||
|
|
||||||
const setCors = (res) => {
|
const setCors = (res) => {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
||||||
|
|
@ -735,6 +740,9 @@ const handleDeleteProject = async (res, projectId) => {
|
||||||
|
|
||||||
const requestHandler = async (req, res) => {
|
const requestHandler = async (req, res) => {
|
||||||
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
||||||
|
if (req.method === "GET" && url.pathname === "/") {
|
||||||
|
console.log("[viewer-request]", truncateLogValue(`${url.pathname}${url.search}`));
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
setCors(res);
|
setCors(res);
|
||||||
|
|
@ -743,6 +751,18 @@ const requestHandler = async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && url.pathname === "/api/client-log") {
|
||||||
|
const payload = await parseJSONBody(req);
|
||||||
|
console.log(
|
||||||
|
"[viewer-client]",
|
||||||
|
payload?.level || "info",
|
||||||
|
payload?.event || "event",
|
||||||
|
truncateLogValue(payload || {})
|
||||||
|
);
|
||||||
|
sendJSON(res, 200, {ok: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api/projects")) {
|
if (url.pathname.startsWith("/api/projects")) {
|
||||||
if (req.method === "GET" && url.pathname === "/api/projects") {
|
if (req.method === "GET" && url.pathname === "/api/projects") {
|
||||||
return handleGetProjects(res, url.searchParams);
|
return handleGetProjects(res, url.searchParams);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue