feat: 接入 mnote web tree shell 与主页链路整理

- 增加 mnote-web tree/command 支持与前端 MnoteWebTreeShell 集成
- 调整 sidebar、documents、runtime config 与 dev/prod server 配套逻辑
- 补充 homepage/tree shell smoke 脚本并更新 harness 进度文件
This commit is contained in:
lix-2026
2026-04-17 23:36:24 +08:00
parent cfc3af8984
commit d8de820d93
40 changed files with 4668 additions and 4143 deletions
+62 -24
View File
@@ -2,33 +2,71 @@ import { useEffect, useState } from "react";
type Status = "idle" | "ok" | "error" | "disabled";
const HEALTH_CACHE_TTL_MS = 15_000;
let cachedStatus: Status = "idle";
let cachedAt = 0;
let inflightCheck: Promise<Status> | null = null;
const listeners = new Set<(status: Status) => void>();
function publishStatus(status: Status) {
cachedStatus = status;
cachedAt = Date.now();
listeners.forEach((listener) => listener(status));
}
async function requestBackendHealth(): Promise<Status> {
const controller = new AbortController();
try {
const response = await fetch("/api/backend/health", { signal: controller.signal });
const payload = (await response.json().catch(() => null)) as { status?: Status } | null;
return payload?.status ?? "error";
} catch {
return "error";
} finally {
controller.abort();
}
}
async function ensureBackendHealthFresh(force = false): Promise<Status> {
const now = Date.now();
if (!force && cachedStatus !== "idle" && now - cachedAt < HEALTH_CACHE_TTL_MS) {
return cachedStatus;
}
if (!inflightCheck) {
inflightCheck = requestBackendHealth()
.then((status) => {
publishStatus(status);
return status;
})
.finally(() => {
inflightCheck = null;
});
}
return inflightCheck;
}
export function useBackendHealth() {
const [status, setStatus] = useState<Status>(() => {
return "idle";
});
const [status, setStatus] = useState<Status>(() => cachedStatus);
useEffect(() => {
let destroyed = false;
const controller = new AbortController();
const check = async () => {
try {
const response = await fetch("/api/backend/health", { signal: controller.signal });
const payload = (await response.json().catch(() => null)) as { status?: Status } | null;
if (!destroyed) {
setStatus(payload?.status ?? "error");
}
} catch {
if (!destroyed) {
setStatus("error");
}
}
};
check();
return () => {
destroyed = true;
controller.abort();
};
}, []);
const listener = (nextStatus: Status) => {
if (!destroyed) {
setStatus(nextStatus);
}
};
listeners.add(listener);
void ensureBackendHealthFresh().then((nextStatus) => {
if (!destroyed) {
setStatus(nextStatus);
}
});
return () => {
destroyed = true;
listeners.delete(listener);
};
}, []);
return status;
}