diff --git a/poc/business-plugins/lib/client.js b/poc/business-plugins/lib/client.js index 9df27f0..f02f770 100644 --- a/poc/business-plugins/lib/client.js +++ b/poc/business-plugins/lib/client.js @@ -162,7 +162,36 @@ window.__ModuleLoader__.load({ "mem.tight": "接近上限", "mem.safe": "余量充足", "mem.overWarn": "勾选后将超出单实例内存上限,实例可能起不来。建议先停用部分插件再应用。", - "mem.perCard": "预估内存" + "mem.perCard": "预估内存", + "pa.label": "平台管理", + "pa.readonly": "只读", + "pa.loading": "加载中…", + "pa.loadingDesc": "正在读取平台状态…", + "pa.notAdminTitle": "平台管理仅对管理员显示", + "pa.notAdminDesc": "当前账号不是管理员,此处不展示平台信息。", + "pa.loadFailed": "平台状态读取失败", + "pa.retry": "重试", + "pa.openPortal": "打开完整管理台", + "pa.readonlyHint": "本分区为只读;写操作请在平台管理台完成。", + "pa.inst": "当前实例", + "pa.running": "运行中", + "pa.stopped": "未运行", + "pa.port": "端口", + "pa.restarts": "自动重启", + "pa.breaker": "熔断", + "pa.breakerOpen": "已熔断(冷却中)", + "pa.breakerOk": "正常", + "pa.users": "用户", + "pa.total": "合计", + "pa.pending": "待审核", + "pa.disabled": "已禁用", + "pa.storage": "存储", + "pa.storageUsers": "有用量的用户", + "pa.storageNote": "报告每小时刷新", + "pa.pool": "候选池", + "pa.poolCount": "已投放插件", + "pa.runtime": "运行时", + "pa.items": "条目" }; /** English dictionary, key-set complete against zh. */ var en = { @@ -208,7 +237,36 @@ window.__ModuleLoader__.load({ "mem.tight": "close to the limit", "mem.safe": "headroom is fine", "mem.overWarn": "The selection exceeds the per-instance memory limit and the instance may fail to start. Disable some plugins first.", - "mem.perCard": "est. memory" + "mem.perCard": "est. memory", + "pa.label": "Platform admin", + "pa.readonly": "read-only", + "pa.loading": "Loading…", + "pa.loadingDesc": "Reading platform status…", + "pa.notAdminTitle": "Platform admin is admin-only", + "pa.notAdminDesc": "This account is not an admin, so platform info is hidden here.", + "pa.loadFailed": "Failed to read platform status", + "pa.retry": "Retry", + "pa.openPortal": "Open full admin console", + "pa.readonlyHint": "This panel is read-only; do write actions in the admin console.", + "pa.inst": "Current instance", + "pa.running": "Running", + "pa.stopped": "Stopped", + "pa.port": "Port", + "pa.restarts": "Auto-restarts", + "pa.breaker": "Circuit breaker", + "pa.breakerOpen": "Open (cooling down)", + "pa.breakerOk": "OK", + "pa.users": "Users", + "pa.total": "Total", + "pa.pending": "Pending", + "pa.disabled": "Disabled", + "pa.storage": "Storage", + "pa.storageUsers": "Users with usage", + "pa.storageNote": "report refreshes hourly", + "pa.pool": "Candidate pool", + "pa.poolCount": "Published plugins", + "pa.runtime": "Runtime", + "pa.items": "items" }; /** @@ -886,6 +944,136 @@ window.__ModuleLoader__.load({ BusinessPluginsSection ); }); + + // ── R2 · 平台管理(只读就地化;档案 81 §9.2b「原生弹窗」形态)──────────────── + // 形态:**原生渲染**(非 iframe)——复用本插件的 token 与 06-工作台UI规范 字号阶梯。 + // 数据:直接调平台只读 API(跨子域 fetch + credentials,门户已加 CORS 白名单)。 + // 范围:**只读子集**(实例 / 用户 / 存储 / 候选池 / 运行时); + // **写操作一律回跳平台管理台** —— 不复制门户的写 UI(否则双源维护 + 扩大执行面)。 + // 可见性:**仅 admin**(先 /api/auth/me 判角色;非 admin 只渲染一行说明)。 + function PaCard(props) { + return jsxRuntime.jsx("div", { + style: { + border: "1px solid " + T.border, borderRadius: "10px", background: T.field, + padding: "12px 14px", display: "flex", flexDirection: "column", gap: "4px", + minWidth: "0", flex: "1 1 150px" + }, + children: [ + jsxRuntime.jsx("div", { key: "t", style: { fontSize: "12px", color: T.dim }, children: props.title }), + jsxRuntime.jsx("div", { key: "v", style: { fontSize: "20px", fontWeight: 500, color: props.color || T.text, fontVariantNumeric: "tabular-nums" }, children: props.value }), + jsxRuntime.jsx("div", { key: "d", style: { fontSize: "12px", color: T.dim, lineHeight: 1.6 }, children: props.desc }) + ] + }); + } + + function PlatformAdminSection() { + var useState = React.useState; + var useEffect = React.useEffect; + var st = useState({ loading: true, err: "", me: null, status: null, users: null, storage: null, pool: null, runtime: null }); + var s = st[0]; + var setS = st[1]; + + function load() { + setS(function (old) { return Object.assign({}, old, { loading: true, err: "" }); }); + var host = portalHost(); + function get(path) { + return fetch(host + path, { credentials: "include" }).then(function (r) { + return r.ok ? r.json() : null; + }).catch(function () { return null; }); + } + get("/api/auth/me").then(function (me) { + var role = me && me.user ? me.user.role : null; + if (role !== "admin") { + setS(function (old) { return Object.assign({}, old, { loading: false, me: me }); }); + return; + } + Promise.all([ + get("/api/dsh/status"), get("/api/admin/users"), + get("/api/admin/storage"), get("/api/plugins/business"), get("/api/admin/runtime") + ]).then(function (r) { + setS({ + loading: false, err: "", me: me, + status: r[0], users: r[1], storage: r[2], pool: r[3], runtime: r[4] + }); + }); + }).catch(function () { + setS(function (old) { return Object.assign({}, old, { loading: false, err: t("pa.loadFailed") }); }); + }); + } + useEffect(function () { load(); }, []); + + var wrap = { padding: "4px 2px", fontSize: "13px", color: T.text, display: "flex", flexDirection: "column", gap: "12px" }; + if (s.loading) { + return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.loading") }), + jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub }, children: t("pa.loadingDesc") }) + ] }); + } + if (s.err) { + return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500, color: T.danger }, children: s.err }), + jsxRuntime.jsx("button", { + key: "b", type: "button", onClick: load, + style: { alignSelf: "flex-start", padding: "6px 14px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.border, background: "transparent", color: T.text, fontSize: "13px" }, + children: t("pa.retry") + }) + ] }); + } + if (!s.me || !s.me.user || s.me.user.role !== "admin") { + return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "h", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.notAdminTitle") }), + jsxRuntime.jsx("div", { key: "d", style: { fontSize: "13px", color: T.sub, lineHeight: 1.6 }, children: t("pa.notAdminDesc") }) + ] }); + } + + var inst = s.status && s.status.running ? t("pa.running") : t("pa.stopped"); + var instDesc = s.status && s.status.instance + ? t("pa.port") + " " + s.status.instance.port + " · " + t("pa.restarts") + " " + (s.status.instance.restarts || 0) + : "—"; + var brk = s.status && s.status.breaker; + var users = (s.users && s.users.users) || []; + var nTotal = users.length; + var nPending = users.filter(function (u) { return u.role === "pending"; }).length; + var nDisabled = users.filter(function (u) { return u.role === "disabled"; }).length; + var stUsers = (s.storage && s.storage.users) || []; + var stDesc = s.storage && s.storage.generatedAt ? t("pa.storageNote") : (s.storage && s.storage.note) || "—"; + var pool = (s.pool && (s.pool.plugins || s.pool.items)) || []; + var rt = (s.runtime && (s.runtime.items || s.runtime.groups)) || []; + + return jsxRuntime.jsx("div", { style: wrap, children: [ + jsxRuntime.jsx("div", { key: "hd", style: { display: "flex", alignItems: "baseline", gap: "8px", flexWrap: "wrap" }, children: [ + jsxRuntime.jsx("span", { key: "a", style: { fontSize: "15px", fontWeight: 500 }, children: t("pa.label") }), + jsxRuntime.jsx("span", { key: "b", style: { fontSize: "12px", color: T.dim }, children: t("pa.readonly") }), + jsxRuntime.jsx("span", { key: "c", style: { fontSize: "12px", color: T.dim, flexBasis: "100%" }, children: t("pa.readonlyHint") }) + ] }), + jsxRuntime.jsx("div", { key: "cards", style: { display: "flex", gap: "10px", flexWrap: "wrap" }, children: [ + jsxRuntime.jsx(PaCard, { key: "i", title: t("pa.inst"), value: inst, color: s.status && s.status.running ? T.success : T.sub, desc: instDesc }), + jsxRuntime.jsx(PaCard, { key: "b", title: t("pa.breaker"), value: brk ? t("pa.breakerOpen") : t("pa.breakerOk"), color: brk ? T.warn : T.success, desc: brk && brk.until ? String(brk.until) : "—" }), + jsxRuntime.jsx(PaCard, { key: "u", title: t("pa.users"), value: nTotal, desc: t("pa.pending") + " " + nPending + " · " + t("pa.disabled") + " " + nDisabled }), + jsxRuntime.jsx(PaCard, { key: "s", title: t("pa.storage"), value: stUsers.length, desc: stDesc }), + jsxRuntime.jsx(PaCard, { key: "p", title: t("pa.pool"), value: pool.length, desc: t("pa.poolCount") }), + jsxRuntime.jsx(PaCard, { key: "r", title: t("pa.runtime"), value: rt.length, desc: t("pa.items") }) + ] }), + jsxRuntime.jsx("button", { + key: "open", type: "button", + onClick: function () { window.open(portalHost() + "/portal.html", "_blank", "noopener"); }, + style: { alignSelf: "flex-start", padding: "7px 16px", borderRadius: "8px", cursor: "pointer", border: "1px solid " + T.primary, background: T.primary, color: "#ffffff", fontSize: "14px" }, + children: t("pa.openPortal") + }) + ] }); + } + + ctx.slots.inject("settings.section", function () { + return ctx.slots.register( + { + name: "settings.section", + id: "platform-admin", + order: 102, + label: function () { return t("pa.label"); } + }, + PlatformAdminSection + ); + }); } exports.apply = apply; diff --git a/poc/business-plugins/package.json b/poc/business-plugins/package.json index 4c144a8..beb1270 100644 --- a/poc/business-plugins/package.json +++ b/poc/business-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@dsh-local/business-plugins", - "version": "0.2.8", + "version": "0.2.9", "description": "功能管理(原「功能插件」)section for dsh web profile — v0.2.8(2026-09-13):**所有弹窗改页内弹窗** —— 弃用 window.confirm,改为 fixed 遮罩 + 居中面板 + 点遮罩/✕ 关闭(视觉与交互对齐 MCN 工作台 dsh-plugin-mcn 的 modalTitle/modalText/modalBtns 三段结构);**超限时弹窗内红底警告块**(标题 + 说明 + 内存数字),确认按钮转危险色,未点确认不发请求。v0.2.7(档案 75 维度 B 落地 · 2026-09-13):卡片新增「预估内存 ≈ N MiB」徽章(按实测成本分色:≥60 红 / ≥30 黄)、卡片加高(minHeight 112 + padding 14/16)便于四层信息排布;**列表上方新增「内存预估」状态条** —— 实心段=当前已启用插件预估占用、半透明段=本次勾选增量、超单实例上限(384 MiB)整条转红并给文字警告,确认弹窗也带上内存预估。口径 = 空载基线 285 MiB + 逐插件实测加载成本(隔离 cgroup 的 rss 增量),**是预估值不是实时读数**。候选池列表 + 搜索/状态徽章 + 批量启用/禁用 + 确认弹窗 + 重启后自动刷新。v0.2.4(档案 67):对齐 06-工作台UI规范 —— **信息层次反转**(主视觉改为插件用途说明,插件名/版本退为副行小字;中文说明由候选池入库时优先取官方目录)、字号阶梯 14/13/12、徽章与按钮改规范量级、空态改「标题 + 说明」、新增列表行 hover 反馈。v0.2.2:分区名由「功能插件」改为「功能管理」。v0.2.0:配色改用官方 --dsw-* design token(跟随 dsh 主题);文案走官方 locale(zh/en)。", "type": "module", "main": "lib/index.js",