feat: add decision center service and cli
This commit is contained in:
@@ -1422,7 +1422,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
.result-card dd { margin: 0; }
|
||||
.result-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
|
||||
.microservice-page, .findjob-page, .pipeline-page, .met-page, .code-queue-page, .baidu-netdisk-page, .filebrowser-page, .oa-event-flow-page {
|
||||
.microservice-page, .findjob-page, .pipeline-page, .met-page, .code-queue-page, .baidu-netdisk-page, .filebrowser-page, .oa-event-flow-page, .decision-center-page {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
@@ -6617,8 +6617,121 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.decision-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 0.36fr);
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.decision-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.decision-filter-bar label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.decision-default-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.decision-card-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.decision-record-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background: rgba(0,0,0,0.16);
|
||||
}
|
||||
.decision-record-card.compact {
|
||||
padding: 9px;
|
||||
}
|
||||
.decision-record-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
.decision-record-head strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
margin-top: 4px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 14px;
|
||||
}
|
||||
.decision-record-meta,
|
||||
.decision-record-foot,
|
||||
.decision-tags,
|
||||
.decision-evidence {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.decision-record-meta span:first-child {
|
||||
color: var(--accent);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.decision-summary {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.decision-markdown {
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.decision-record-foot {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.decision-record-foot code {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.decision-tags span {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid rgba(78, 183, 168, 0.34);
|
||||
color: var(--accent-2);
|
||||
background: rgba(78, 183, 168, 0.08);
|
||||
font-size: 11px;
|
||||
}
|
||||
.decision-evidence a {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.decision-table th,
|
||||
.decision-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.decision-table td strong,
|
||||
.decision-table td code {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.mdtodo-layout {
|
||||
.mdtodo-layout,
|
||||
.decision-hero,
|
||||
.decision-filter-bar,
|
||||
.decision-default-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { BaiduNetdiskPage } from "./baidu-netdisk";
|
||||
import { ClaudeQqPage } from "./claudeqq";
|
||||
import { CodeQueuePage } from "./code-queue";
|
||||
import { DecisionCenterPage } from "./decision-center";
|
||||
import { FileBrowserPage } from "./filebrowser";
|
||||
import { FindJobPage } from "./findjob";
|
||||
import { MetNonlinearPage } from "./met-nonlinear";
|
||||
@@ -1652,6 +1653,7 @@ function MicroserviceCatalogPage({ microservices, onRaw, onNavigate }: AnyRecord
|
||||
service.id === "k3sctl-adapter" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "k3sctl"), "data-testid": "open-k3sctl-button" }, "打开") : null,
|
||||
service.id === "code-queue" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "open-code-queue-button" }, "打开") : null,
|
||||
service.id === "mdtodo" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "mdtodo"), "data-testid": "open-mdtodo-button" }, "打开") : null,
|
||||
service.id === "decision-center" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "decision-center"), "data-testid": "open-decision-center-button" }, "打开") : null,
|
||||
service.id === "project-manager" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "project-manager"), "data-testid": "open-project-manager-button" }, "打开") : null,
|
||||
h(RawButton, { title: `用户服务 ${service.id}`, data: service, onOpen: onRaw }),
|
||||
),
|
||||
@@ -2160,6 +2162,7 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa
|
||||
if (activeModule === "apps" && activeTab === "k3sctl") return h(K3sCtlPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, onNavigate });
|
||||
if (activeModule === "apps" && activeTab === "code-queue") return h(CodeQueuePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, initialTasksData: initialCodeQueueOverview });
|
||||
if (activeModule === "apps" && activeTab === "mdtodo") return h(MdtodoPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "decision-center") return h(DecisionCenterPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "apps" && activeTab === "project-manager") return h(ProjectManagerPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
|
||||
if (activeModule === "config" && activeTab === "topology") return h(TopologyPage, { data });
|
||||
if (activeModule === "config" && activeTab === "auth") return h(AuthPage, { session });
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import React from "react";
|
||||
import { fmtClock, fmtDate } from "./time";
|
||||
import { LoadingTitle } from "./loading-indicator";
|
||||
import { MarkdownBody } from "./markdown";
|
||||
import { errorMessage, requestJson } from "./unidesk-error";
|
||||
import { UniDeskErrorBanner } from "./unidesk-error-banner";
|
||||
|
||||
type AnyRecord = Record<string, any>;
|
||||
|
||||
const h = React.createElement;
|
||||
const { useEffect } = React;
|
||||
const useState: any = React.useState;
|
||||
|
||||
const recordTypes = ["all", "meeting", "decision", "goal", "blocker", "debt", "experiment"];
|
||||
const recordLevels = ["all", "G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "none"];
|
||||
const recordStatuses = ["all", "active", "blocked", "parked", "done"];
|
||||
|
||||
function StatusBadge({ status, children }: AnyRecord) {
|
||||
const normalized = String(status || "unknown").toLowerCase();
|
||||
return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown");
|
||||
}
|
||||
|
||||
function MetricCard({ label, value, hint, tone }: AnyRecord) {
|
||||
return h("article", { className: `metric-card ${tone || ""}` },
|
||||
h("div", { className: "metric-label" }, label),
|
||||
h("div", { className: "metric-value" }, value),
|
||||
h("div", { className: "metric-hint" }, hint),
|
||||
);
|
||||
}
|
||||
|
||||
function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) {
|
||||
return h("section", { className: `panel ${className || ""}` },
|
||||
h("div", { className: "panel-head" },
|
||||
h("div", null,
|
||||
eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null,
|
||||
h(LoadingTitle, { title, loading }),
|
||||
),
|
||||
actions ? h("div", { className: "panel-actions" }, actions) : null,
|
||||
),
|
||||
h("div", { className: "panel-body" }, children),
|
||||
);
|
||||
}
|
||||
|
||||
function RawButton({ title, data, onOpen, testId }: AnyRecord) {
|
||||
return h("button", {
|
||||
type: "button",
|
||||
className: "ghost-btn",
|
||||
"data-testid": testId,
|
||||
onClick: () => onOpen(title, data),
|
||||
}, "查看原始JSON");
|
||||
}
|
||||
|
||||
function EmptyState({ title, text }: AnyRecord) {
|
||||
return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text));
|
||||
}
|
||||
|
||||
function microserviceRuntime(service: any): AnyRecord {
|
||||
return service?.runtime && typeof service.runtime === "object" && !Array.isArray(service.runtime) ? service.runtime : {};
|
||||
}
|
||||
|
||||
function microserviceBackend(service: any): AnyRecord {
|
||||
return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {};
|
||||
}
|
||||
|
||||
function microserviceRepository(service: any): AnyRecord {
|
||||
return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {};
|
||||
}
|
||||
|
||||
function decisionApi(apiBaseUrl: string, path: string): string {
|
||||
return `${apiBaseUrl}/microservices/decision-center/proxy${path}`;
|
||||
}
|
||||
|
||||
function levelTone(level: string): string {
|
||||
if (level === "G0" || level === "G1") return "online";
|
||||
if (level === "P0" || level === "P1") return "failed";
|
||||
if (level === "none") return "unknown";
|
||||
return "warn";
|
||||
}
|
||||
|
||||
function statusTone(status: string): string {
|
||||
if (status === "done") return "online";
|
||||
if (status === "blocked") return "failed";
|
||||
if (status === "parked") return "warn";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function fmtRecordTime(value: any): string {
|
||||
return fmtDate(value) || "--";
|
||||
}
|
||||
|
||||
function shortText(value: any, max = 220): string {
|
||||
const text = String(value || "").replace(/\s+/gu, " ").trim();
|
||||
return text.length > max ? `${text.slice(0, max - 1)}...` : text;
|
||||
}
|
||||
|
||||
function RecordCard({ record, onRaw, compact }: AnyRecord) {
|
||||
const tags = Array.isArray(record.tags) ? record.tags : [];
|
||||
const evidence = Array.isArray(record.evidenceLinks) ? record.evidenceLinks : [];
|
||||
return h("article", { className: `decision-record-card ${compact ? "compact" : ""}`, "data-testid": `decision-record-${String(record.id || "").replace(/[^A-Za-z0-9_-]+/g, "-")}` },
|
||||
h("div", { className: "decision-record-head" },
|
||||
h("div", null,
|
||||
h("div", { className: "decision-record-meta" },
|
||||
h("span", null, record.type || "--"),
|
||||
h(StatusBadge, { status: levelTone(record.level) }, record.level || "none"),
|
||||
h(StatusBadge, { status: statusTone(record.status) }, record.status || "--"),
|
||||
),
|
||||
h("strong", null, record.title || "--"),
|
||||
),
|
||||
h(RawButton, { title: `Decision ${record.id}`, data: record, onOpen: onRaw }),
|
||||
),
|
||||
compact
|
||||
? h("p", { className: "decision-summary" }, shortText(record.summary || record.body))
|
||||
: h(MarkdownBody, { markdown: record.body || record.summary || "", className: "decision-markdown" }),
|
||||
h("div", { className: "decision-record-foot" },
|
||||
record.linkedGoalId ? h("code", null, `goal:${record.linkedGoalId}`) : null,
|
||||
record.taskId ? h("code", null, `task:${record.taskId}`) : null,
|
||||
record.commitId ? h("code", null, record.commitId.slice(0, 12)) : null,
|
||||
h("span", null, fmtRecordTime(record.updatedAt)),
|
||||
),
|
||||
tags.length > 0 ? h("div", { className: "decision-tags" }, tags.slice(0, 8).map((tag: string) => h("span", { key: tag }, tag))) : null,
|
||||
evidence.length > 0 ? h("div", { className: "decision-evidence" }, evidence.slice(0, 4).map((link: string) => h("a", { key: link, href: link, target: "_blank", rel: "noreferrer" }, shortText(link, 58)))) : null,
|
||||
);
|
||||
}
|
||||
|
||||
function RecordTable({ records, onRaw }: AnyRecord) {
|
||||
if (!records.length) return h(EmptyState, { title: "暂无记录", text: "通过 CLI 上传会议记录或决议后会显示在这里。" });
|
||||
return h("div", { className: "table-wrap" },
|
||||
h("table", { className: "decision-table", "data-testid": "decision-center-record-table" },
|
||||
h("thead", null, h("tr", null,
|
||||
h("th", null, "等级"),
|
||||
h("th", null, "状态"),
|
||||
h("th", null, "类型"),
|
||||
h("th", null, "标题"),
|
||||
h("th", null, "摘要"),
|
||||
h("th", null, "证据"),
|
||||
h("th", null, "更新"),
|
||||
h("th", null, "操作"),
|
||||
)),
|
||||
h("tbody", null, records.map((record: any) => h("tr", { key: record.id },
|
||||
h("td", null, h(StatusBadge, { status: levelTone(record.level) }, record.level || "none")),
|
||||
h("td", null, h(StatusBadge, { status: statusTone(record.status) }, record.status || "--")),
|
||||
h("td", null, record.type || "--"),
|
||||
h("td", null, h("strong", null, record.title || "--"), record.linkedGoalId ? h("code", null, record.linkedGoalId) : null),
|
||||
h("td", null, shortText(record.summary || record.body, 180)),
|
||||
h("td", null, Array.isArray(record.evidenceLinks) ? record.evidenceLinks.length : 0),
|
||||
h("td", null, fmtRecordTime(record.updatedAt)),
|
||||
h("td", null, h(RawButton, { title: `Decision ${record.id}`, data: record, onOpen: onRaw })),
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function selectOptions(values: string[]): any[] {
|
||||
return values.map((value) => h("option", { key: value, value }, value));
|
||||
}
|
||||
|
||||
function filteredQuery(filters: AnyRecord): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.type !== "all") params.set("type", filters.type);
|
||||
if (filters.status !== "all") params.set("status", filters.status);
|
||||
if (filters.level !== "all") params.set("level", filters.level);
|
||||
if (filters.linkedGoalId.trim()) params.set("linkedGoalId", filters.linkedGoalId.trim());
|
||||
params.set("limit", "240");
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export function DecisionCenterPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) {
|
||||
const service = microservices.find((item: any) => item.id === "decision-center") || null;
|
||||
const [state, setState] = useState({ loading: false, error: "", health: null, records: [], refreshedAt: null });
|
||||
const [filters, setFilters] = useState({ type: "all", status: "all", level: "all", linkedGoalId: "" });
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (!service) return;
|
||||
setState((prev: any) => ({ ...prev, loading: true, error: "" }));
|
||||
try {
|
||||
const query = filteredQuery(filters);
|
||||
const [health, records] = await Promise.all([
|
||||
requestJson(`${apiBaseUrl}/microservices/decision-center/health`),
|
||||
requestJson(decisionApi(apiBaseUrl, `/api/records?${query}`)),
|
||||
]);
|
||||
setState({ loading: false, error: "", health, records: Array.isArray(records.records) ? records.records : [], refreshedAt: new Date() });
|
||||
} catch (err) {
|
||||
setState((prev: any) => ({ ...prev, loading: false, error: errorMessage(err, "Decision Center 加载失败") }));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [service?.id, service?.runtime?.providerStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => void load(), 120);
|
||||
return () => clearTimeout(timer);
|
||||
}, [filters.type, filters.status, filters.level, filters.linkedGoalId]);
|
||||
|
||||
if (!service) return h(EmptyState, { title: "Decision Center 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=decision-center" });
|
||||
|
||||
const runtime = microserviceRuntime(service);
|
||||
const repository = microserviceRepository(service);
|
||||
const backend = microserviceBackend(service);
|
||||
const records = Array.isArray(state.records) ? state.records : [];
|
||||
const goals = records.filter((record: any) => record.type === "goal" && ["G0", "G1"].includes(record.level) && record.status !== "done").slice(0, 8);
|
||||
const blockers = records.filter((record: any) => record.type === "blocker" && ["P0", "P1"].includes(record.level) && record.status !== "done").slice(0, 8);
|
||||
const parked = records.filter((record: any) => record.status === "parked").slice(0, 8);
|
||||
const recentMeetings = records.filter((record: any) => record.type === "meeting" || record.type === "decision").slice(0, 12);
|
||||
|
||||
return h("div", { className: "decision-center-page", "data-testid": "decision-center-page" },
|
||||
h(Panel, { title: "Decision Center", eyebrow: "Authority Records", loading: state.loading, actions: h("div", { className: "inline-actions" },
|
||||
h("button", { type: "button", className: "ghost-btn", onClick: () => void load(), disabled: state.loading }, state.loading ? "刷新中" : "刷新"),
|
||||
h(RawButton, { title: "Decision Center Health", data: state.health, onOpen: onRaw, testId: "raw-decision-center-health" }),
|
||||
) },
|
||||
h("div", { className: "decision-hero" },
|
||||
h("div", { className: "metric-grid" },
|
||||
h(MetricCard, { label: "记录数", value: records.length, hint: `PostgreSQL / ${state.health?.storage || "postgres"}`, tone: "ok" }),
|
||||
h(MetricCard, { label: "G0/G1 目标", value: goals.length, hint: "active authority goals", tone: "ok" }),
|
||||
h(MetricCard, { label: "P0/P1 Blocker", value: blockers.length, hint: "requires decision", tone: blockers.length > 0 ? "warn" : "ok" }),
|
||||
h(MetricCard, { label: "Parked", value: parked.length, hint: "停放事项", tone: parked.length > 0 ? "warn" : "ok" }),
|
||||
),
|
||||
h("div", { className: "microservice-ref-card" },
|
||||
h("span", null, "Runtime"),
|
||||
h("strong", null, runtime.orchestrator || service.deployment?.mode || "k3sctl"),
|
||||
h("code", null, `${service.providerId} / ${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`),
|
||||
h("code", null, repository.commitId || "--"),
|
||||
),
|
||||
),
|
||||
h(UniDeskErrorBanner, { error: state.error, title: "Decision Center 请求失败" }),
|
||||
),
|
||||
h(Panel, { title: "筛选", eyebrow: "Type / Status / Level" },
|
||||
h("div", { className: "decision-filter-bar", "data-testid": "decision-center-filters" },
|
||||
h("label", null, "类型", h("select", { value: filters.type, onChange: (event: any) => setFilters((prev: any) => ({ ...prev, type: event.target.value })) }, selectOptions(recordTypes))),
|
||||
h("label", null, "状态", h("select", { value: filters.status, onChange: (event: any) => setFilters((prev: any) => ({ ...prev, status: event.target.value })) }, selectOptions(recordStatuses))),
|
||||
h("label", null, "等级", h("select", { value: filters.level, onChange: (event: any) => setFilters((prev: any) => ({ ...prev, level: event.target.value })) }, selectOptions(recordLevels))),
|
||||
h("label", null, "Linked Goal", h("input", { value: filters.linkedGoalId, onChange: (event: any) => setFilters((prev: any) => ({ ...prev, linkedGoalId: event.target.value })), placeholder: "goal id" })),
|
||||
),
|
||||
),
|
||||
h("div", { className: "decision-default-grid" },
|
||||
h(Panel, { title: "G0/G1 目标", eyebrow: `${goals.length} Goals` },
|
||||
goals.length === 0 ? h(EmptyState, { title: "暂无当前目标", text: "目标记录使用 type=goal 且 level=G0/G1。" }) :
|
||||
h("div", { className: "decision-card-list" }, goals.map((record: any) => h(RecordCard, { key: record.id, record, onRaw, compact: true }))),
|
||||
),
|
||||
h(Panel, { title: "P0/P1 Blocker", eyebrow: `${blockers.length} Blockers` },
|
||||
blockers.length === 0 ? h(EmptyState, { title: "暂无高优先级阻塞", text: "阻塞记录使用 type=blocker 且 level=P0/P1。" }) :
|
||||
h("div", { className: "decision-card-list" }, blockers.map((record: any) => h(RecordCard, { key: record.id, record, onRaw, compact: true }))),
|
||||
),
|
||||
h(Panel, { title: "停放事项", eyebrow: `${parked.length} Parked` },
|
||||
parked.length === 0 ? h(EmptyState, { title: "暂无停放事项", text: "status=parked 的记录会集中展示。" }) :
|
||||
h("div", { className: "decision-card-list" }, parked.map((record: any) => h(RecordCard, { key: record.id, record, onRaw, compact: true }))),
|
||||
),
|
||||
h(Panel, { title: "最近会议/决议", eyebrow: `${recentMeetings.length} Recent` },
|
||||
recentMeetings.length === 0 ? h(EmptyState, { title: "暂无会议或决议", text: "使用 CLI 上传 Markdown 会议记录后会显示。" }) :
|
||||
h("div", { className: "decision-card-list" }, recentMeetings.map((record: any) => h(RecordCard, { key: record.id, record, onRaw, compact: true }))),
|
||||
),
|
||||
),
|
||||
h(Panel, { title: "全部记录", eyebrow: `${records.length} Records`, actions: state.refreshedAt ? h("span", { className: "muted" }, `刷新 ${fmtClock(state.refreshedAt)}`) : null },
|
||||
h(RecordTable, { records, onRaw }),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,12 @@ interface RuntimeConfig {
|
||||
sessionSecret: string;
|
||||
sessionTtlSeconds: number;
|
||||
logFile: string;
|
||||
deploy: {
|
||||
serviceId: string;
|
||||
repo: string;
|
||||
commit: string;
|
||||
requestedCommit: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SessionPayload {
|
||||
@@ -143,6 +149,12 @@ function readConfig(): RuntimeConfig {
|
||||
sessionSecret: requiredEnv("SESSION_SECRET"),
|
||||
sessionTtlSeconds: readNumberEnv("SESSION_TTL_SECONDS"),
|
||||
logFile: requiredEnv("LOG_FILE"),
|
||||
deploy: {
|
||||
serviceId: process.env.UNIDESK_DEPLOY_SERVICE_ID || "frontend",
|
||||
repo: process.env.UNIDESK_DEPLOY_REPO || "",
|
||||
commit: process.env.UNIDESK_DEPLOY_COMMIT || "",
|
||||
requestedCommit: process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT || "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -711,7 +723,7 @@ async function handleRequest(req: Request): Promise<Response> {
|
||||
logger("debug", "request", { path: url.pathname });
|
||||
try {
|
||||
if (url.pathname === "/health") {
|
||||
return jsonResponse({ ok: true, service: "unidesk-frontend", frontendPublicUrl: config.frontendPublicUrl });
|
||||
return jsonResponse({ ok: true, service: "unidesk-frontend", frontendPublicUrl: config.frontendPublicUrl, deploy: config.deploy });
|
||||
}
|
||||
if (url.pathname === "/login" && req.method === "POST") return login(req);
|
||||
if (url.pathname === "/logout" && req.method === "POST") return logout();
|
||||
|
||||
@@ -73,6 +73,7 @@ export const MODULES: UniDeskModuleDefinition[] = [
|
||||
{ id: "k3sctl", label: "k3s Control" },
|
||||
{ id: "code-queue", label: "Code Queue" },
|
||||
{ id: "mdtodo", label: "MDTODO" },
|
||||
{ id: "decision-center", label: "Decision Center" },
|
||||
{ id: "project-manager", label: "Project Manager" },
|
||||
] },
|
||||
{ id: "config", label: "系统配置", code: "CFG", tabs: [
|
||||
|
||||
Reference in New Issue
Block a user