feat: integrate todo note microservice and modularize frontend

This commit is contained in:
Codex
2026-05-05 10:33:26 +00:00
parent abd40fa252
commit 1d0046dc50
28 changed files with 2121 additions and 381 deletions
+50 -4
View File
@@ -32,6 +32,8 @@ interface RuntimeConfig {
providerToken: string;
heartbeatTimeoutMs: number;
taskPendingTimeoutMs: number;
databaseVolumeName: string;
databaseVolumeSize: string;
microservices: MicroserviceConfig[];
logFile: string;
}
@@ -56,6 +58,7 @@ interface MicroserviceConfig {
proxyMode: string;
frontendOnly: boolean;
public: boolean;
allowedMethods: string[];
allowedPathPrefixes: string[];
healthPath: string;
timeoutMs: number;
@@ -180,6 +183,7 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon
proxyMode: stringFromRecord(backend, "proxyMode", `${path}.backend`),
frontendOnly: booleanFromRecord(backend, "frontendOnly", `${path}.backend`),
public: booleanFromRecord(backend, "public", `${path}.backend`),
allowedMethods: stringArrayFromRecord(backend, "allowedMethods", `${path}.backend`).map((method) => method.toUpperCase()),
allowedPathPrefixes: stringArrayFromRecord(backend, "allowedPathPrefixes", `${path}.backend`),
healthPath: stringFromRecord(backend, "healthPath", `${path}.backend`),
timeoutMs: numberFromRecord(backend, "timeoutMs", `${path}.backend`),
@@ -212,6 +216,8 @@ function readConfig(): RuntimeConfig {
providerToken: requiredEnv("PROVIDER_TOKEN"),
heartbeatTimeoutMs: readNumberEnv("HEARTBEAT_TIMEOUT_MS"),
taskPendingTimeoutMs: readOptionalNumberEnv("TASK_PENDING_TIMEOUT_MS", 10 * 60 * 1000),
databaseVolumeName: requiredEnv("DATABASE_VOLUME_NAME"),
databaseVolumeSize: requiredEnv("DATABASE_VOLUME_SIZE"),
microservices: readMicroservicesEnv(),
logFile: requiredEnv("LOG_FILE"),
};
@@ -242,7 +248,7 @@ function jsonResponse(body: unknown, status = 200): Response {
headers: {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET,POST,OPTIONS",
"access-control-allow-methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
"access-control-allow-headers": "content-type,x-provider-token",
},
});
@@ -832,16 +838,35 @@ async function countPendingTasks(): Promise<number> {
return Number(rows[0]?.count ?? 0);
}
async function getPgdataUsage(): Promise<JsonValue> {
const rows = await sql<Array<{ database_name: string; database_bytes: string | number; database_pretty: string }>>`
SELECT
current_database()::text AS database_name,
pg_database_size(current_database())::bigint AS database_bytes,
pg_size_pretty(pg_database_size(current_database()))::text AS database_pretty
`;
const row = rows[0];
return {
volumeName: config.databaseVolumeName,
volumeSize: config.databaseVolumeSize,
databaseName: row?.database_name ?? "unknown",
databaseBytes: Number(row?.database_bytes ?? 0),
databasePretty: row?.database_pretty ?? "--",
};
}
async function getOverview(): Promise<JsonValue> {
const nodes = await getNodes();
const pendingTasks = await countPendingTasks();
const dockerStatuses = await getNodeDockerStatuses();
const systemStatuses = await getNodeSystemStatuses(1);
const pgdata = await getPgdataUsage();
const online = nodes.filter((node) => node.status === "online").length;
return {
service: "unidesk-core",
ok: true,
dbReady,
pgdata,
uptimeSeconds: Math.floor((Date.now() - serviceStartedAt.getTime()) / 1000),
nodeCount: nodes.length,
onlineNodeCount: online,
@@ -948,6 +973,10 @@ function isMicroservicePathAllowed(service: MicroserviceConfig, path: string): b
return service.backend.allowedPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix));
}
function isMicroserviceMethodAllowed(service: MicroserviceConfig, method: string): boolean {
return service.backend.allowedMethods.includes(method.toUpperCase());
}
function readMicroserviceArrayLimits(url: URL): { query: string; jsonArrayLimits: Record<string, JsonValue> } {
const params = new URLSearchParams(url.searchParams);
const jsonArrayLimits: Record<string, JsonValue> = {};
@@ -988,8 +1017,10 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
const suffix = slashIndex === -1 ? "" : rest.slice(slashIndex + 1);
const service = microserviceById(serviceId);
if (service === null) return jsonResponse({ ok: false, error: `microservice not found: ${serviceId}` }, 404);
if (suffix === "" || suffix === "status") return jsonResponse({ ok: true, microservice: (await getMicroservices()).find((item) => recordValue(item, "id") === serviceId) ?? service });
if (req.method !== "GET" && req.method !== "HEAD") return jsonResponse({ ok: false, error: "microservice frontend proxy only supports GET/HEAD" }, 405);
const method = req.method.toUpperCase();
if ((suffix === "" || suffix === "status") && (method === "GET" || method === "HEAD")) {
return jsonResponse({ ok: true, microservice: (await getMicroservices()).find((item) => recordValue(item, "id") === serviceId) ?? service });
}
const proxyPrefix = "proxy";
const targetPath = suffix === "health"
@@ -1000,6 +1031,12 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
? `/${suffix.slice(proxyPrefix.length + 1)}`
: "";
if (targetPath.length === 0) return jsonResponse({ ok: false, error: "microservice route must be /status, /health, or /proxy/<path>" }, 404);
if (suffix === "health" && method !== "GET" && method !== "HEAD") {
return jsonResponse({ ok: false, error: "microservice health only supports GET/HEAD" }, 405);
}
if (!isMicroserviceMethodAllowed(service, method)) {
return jsonResponse({ ok: false, error: "microservice method is not allowed", serviceId, method, allowedMethods: service.backend.allowedMethods }, 405);
}
if (!isMicroservicePathAllowed(service, targetPath)) {
return jsonResponse({ ok: false, error: "microservice path is not allowed", serviceId, targetPath }, 403);
}
@@ -1007,13 +1044,22 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
}
const proxyOptions = readMicroserviceArrayLimits(url);
const bodyText = method === "GET" || method === "HEAD" ? "" : await req.text();
if (bodyText.length > 1024 * 1024) {
return jsonResponse({ ok: false, error: "microservice request body is too large", maxBytes: 1024 * 1024 }, 413);
}
const requestHeaders: Record<string, JsonValue> = {};
const contentType = req.headers.get("content-type");
if (contentType !== null) requestHeaders["content-type"] = contentType.slice(0, 200);
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
source: "microservice-frontend-proxy",
serviceId: service.id,
method: req.method,
method,
targetBaseUrl: service.backend.nodeBaseUrl,
path: targetPath,
query: proxyOptions.query,
requestHeaders,
bodyText,
jsonArrayLimits: proxyOptions.jsonArrayLimits,
timeoutMs: service.backend.timeoutMs,
});