feat: 完成 issue #135 管理控制台(web/admin:模型/知识库/告警确认/审计查询)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# web/admin — iAOP 管理控制台
|
||||
|
||||
issue #135 / PRD 8.1/8.2。纯静态单页(无构建链):
|
||||
模型管理 / 知识库管理 / 告警确认 / 审计查询。
|
||||
|
||||
## 使用
|
||||
|
||||
```bash
|
||||
# 从 web/ 根目录起服务(依赖 ../auth 守卫与 ../chat/citations.json、
|
||||
# ../cockpit/plans/alarm_panel.ti.json 相对路径)
|
||||
cd web && python -m http.server 8084
|
||||
# 打开 http://127.0.0.1:8084/admin/index.html
|
||||
```
|
||||
|
||||
加载到 `../auth/auth.js` 时强制登录(未登录跳登录页);纯静态独立运行时降级免登录演示。
|
||||
|
||||
## 页面与后端语义对齐
|
||||
|
||||
| 页签 | 能力 | 对齐 |
|
||||
|------|------|------|
|
||||
| 模型管理 | 模板列表、版本/阶段视图(dev/staging/prod 徽标)、注册新版本(semver 递增校验)、promote 逐级提升、rollback 回退、全程审计 | `core/model-framework/template_registry.py`(Stage/next_stage/_log) |
|
||||
| 知识库管理 | 文档列表(标题/来源/章节数/索引状态)、上传并索引(浏览器端)、来源引用查看 | `core/rag-kb/` 文档资产 + `web/chat/citations.json` 编译产物 |
|
||||
| 告警确认 | 告警列表(P0/P1/P2 配色取自 `alarm_panel.ti.json`)、详情(LLM 解释 + SOP)、状态机 待处理→已确认→已闭环(P0 先确认后闭环) | PRD 8.1 Alert 状态机、`alert_rules.py` severity、`alarm_config.py` 配色 |
|
||||
| 审计查询 | 统一审计日志(时间/操作者/动作/资源/原因)、关键词+动作筛选、导出 JSON | `template_registry._log` / `prompts.drain_audit` 风格(#134 UserStore 审计) |
|
||||
|
||||
## 边界
|
||||
|
||||
数据持久化在 localStorage(Demo 级;模型/告警含 Ti 示例种子数据);
|
||||
后端 HTTP 服务就绪后替换各 Store 读写即可,页面结构不变。
|
||||
LLM 解释直连推理服务 `http://39.101.182.167:30800/v1/chat/completions`。
|
||||
@@ -0,0 +1,67 @@
|
||||
/* iAOP 管理控制台样式(issue #135)。深色主题,与 cockpit/studio 一致。 */
|
||||
:root {
|
||||
--bg: #0b1220; --surface: #13203a; --fg: #e6edf6; --muted: #8aa0bd;
|
||||
--accent: #18d3c8; --warn: #f5a623; --line: rgba(255,255,255,.06);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
background: var(--bg); color: var(--fg); }
|
||||
|
||||
#topbar { display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 16px; background: var(--surface); border-bottom: 1px solid var(--line); }
|
||||
h1 { font-size: 18px; color: var(--accent); margin: 0; }
|
||||
h1 .sub { font-size: 12px; color: var(--muted); font-weight: 400; }
|
||||
#topbar-right { display: flex; gap: 14px; align-items: center; font-size: 13px; }
|
||||
#topbar-right a { color: var(--accent); text-decoration: none; }
|
||||
.hint { font-size: 11px; color: #64748b; }
|
||||
|
||||
#tabs { display: flex; gap: 4px; padding: 8px 16px 0; }
|
||||
.tab { padding: 8px 18px; border: 1px solid var(--line); border-bottom: none;
|
||||
border-radius: 8px 8px 0 0; background: transparent; color: var(--muted);
|
||||
cursor: pointer; font-size: 14px; }
|
||||
.tab.active { background: var(--surface); color: var(--accent); }
|
||||
|
||||
.page { display: none; padding: 12px 16px 24px; }
|
||||
.page.active { display: block; }
|
||||
.panel { background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: 8px; padding: 14px 16px; }
|
||||
.panel h2 { font-size: 14px; margin: 0 0 10px; color: var(--accent); }
|
||||
.row { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }
|
||||
|
||||
select, input { padding: 7px 10px; border-radius: 6px; font-size: 13px;
|
||||
border: 1px solid var(--line); background: var(--bg); color: var(--fg); }
|
||||
button { padding: 6px 14px; border: none; border-radius: 6px; cursor: pointer;
|
||||
background: #1e293b; color: var(--fg); font-size: 12px; }
|
||||
button.primary { background: var(--accent); color: #04202a; }
|
||||
button:disabled { opacity: .4; cursor: not-allowed; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }
|
||||
th, td { padding: 7px 8px; border-bottom: 1px solid var(--line); text-align: left; }
|
||||
th { color: var(--muted); font-weight: 400; }
|
||||
|
||||
/* 模型阶段徽标 */
|
||||
.stage { padding: 2px 10px; border-radius: 10px; font-size: 12px; }
|
||||
.stage-dev { background: rgba(138,160,189,.15); color: var(--muted); }
|
||||
.stage-staging { background: rgba(245,166,35,.15); color: var(--warn); }
|
||||
.stage-prod { background: rgba(46,204,113,.15); color: #2ecc71; }
|
||||
|
||||
/* 知识库 */
|
||||
.detail { margin-top: 12px; background: var(--bg); border-radius: 6px; padding: 10px;
|
||||
font-size: 12px; color: var(--muted); max-height: 260px; overflow-y: auto; }
|
||||
.idx-ok { color: #2ecc71; }
|
||||
.idx-pending { color: var(--warn); }
|
||||
|
||||
/* 告警 */
|
||||
.split-alerts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.alert-item { border-radius: 6px; padding: 8px 12px; margin-bottom: 6px; font-size: 13px;
|
||||
border-left: 3px solid transparent; cursor: pointer;
|
||||
display: flex; gap: 10px; align-items: center; }
|
||||
.alert-item:hover { outline: 1px solid var(--line); }
|
||||
.alert-item .sev { font-weight: 700; }
|
||||
.alert-item .st { margin-left: auto; font-size: 11px; color: var(--muted); }
|
||||
#alert-detail { font-size: 13px; line-height: 1.7; }
|
||||
#alert-detail .llm { background: var(--bg); border-radius: 6px; padding: 10px;
|
||||
white-space: pre-wrap; margin: 8px 0; }
|
||||
#alert-detail .sop { border-left: 3px solid var(--warn); padding: 4px 10px;
|
||||
color: var(--muted); font-size: 12px; }
|
||||
@media (max-width: 1000px) { .split-alerts { grid-template-columns: 1fr; } }
|
||||
@@ -0,0 +1,368 @@
|
||||
/* iAOP 管理控制台(issue #135 / PRD 8.1/8.2)。
|
||||
*
|
||||
* 纯静态单页(无构建链),语义对齐后端内核:
|
||||
* - 模型管理:core/model-framework/template_registry.py —— semver 校验、
|
||||
* dev→staging→prod 逐级 promote、rollback 回退、全程 _log 审计;
|
||||
* - 知识库管理:文档列表来自 web/chat/citations.json(模板 rag-kb 资产编译产物),
|
||||
* 上传在浏览器端索引(Demo;rag-kb HTTP 服务就绪后替换为真实索引管线);
|
||||
* - 告警确认:PRD 8.1 状态机 pending→acked→closed(P0 必须先确认再闭环,
|
||||
* 高利害人工确认),severity 配色取自 cockpit 的 alarm_panel.ti.json;
|
||||
* - 审计查询:统一读取 UserStore 审计日志(#134),支持筛选与导出。
|
||||
*
|
||||
* 数据持久化在 localStorage(Demo 级);后端 HTTP 服务就绪后替换 Store 读写即可。
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var INFER_BASE = "http://39.101.182.167:30800";
|
||||
var MODELS_KEY = "iaop.admin.models.v1";
|
||||
var ALERTS_KEY = "iaop.admin.alerts.v1";
|
||||
var KB_KEY = "iaop.admin.kb.v1";
|
||||
|
||||
var currentUserName = "anonymous";
|
||||
var severityStyles = {
|
||||
P0: { fg: "#ff3b30", bg: "rgba(255,59,48,0.12)", border: "#ff3b30" },
|
||||
P1: { fg: "#f5a623", bg: "rgba(245,166,35,0.12)", border: "#f5a623" },
|
||||
P2: { fg: "#3aa0ff", bg: "rgba(58,160,255,0.10)" }
|
||||
};
|
||||
|
||||
/* ---------------- 工具 ---------------- */
|
||||
function el(tag, cls, text) {
|
||||
var n = document.createElement(tag);
|
||||
if (cls) n.className = cls;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
function load(key, fallback) {
|
||||
try { var v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
|
||||
catch (e) { return fallback; }
|
||||
}
|
||||
function save(key, obj) { localStorage.setItem(key, JSON.stringify(obj)); }
|
||||
function audit(action, resource, reason) {
|
||||
if (typeof UserStore !== "undefined") UserStore.audit(action, resource, reason, currentUserName);
|
||||
}
|
||||
function download(filename, text) {
|
||||
var a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob([text], { type: "application/json" }));
|
||||
a.download = filename; a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
/* ---------------- 模型管理(template_registry 语义) ---------------- */
|
||||
var STAGES = ["dev", "staging", "prod"];
|
||||
var SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
||||
function semverKey(v) {
|
||||
var p = v.split(".").map(Number);
|
||||
return p[0] * 1e6 + p[1] * 1e3 + p[2];
|
||||
}
|
||||
function seedModels() {
|
||||
return [
|
||||
{ name: "quality_forecast", version: "1.2.0", stage: "prod", author: "bot_dev1",
|
||||
updated_at: "2026-08-01T09:00:00Z" },
|
||||
{ name: "anomaly_detection", version: "1.0.3", stage: "staging", author: "bot_dev1",
|
||||
updated_at: "2026-08-02T09:00:00Z" },
|
||||
{ name: "process_optimizer", version: "0.9.1", stage: "dev", author: "engineer",
|
||||
updated_at: "2026-08-03T09:00:00Z" },
|
||||
{ name: "cross_process_optimizer", version: "0.5.0", stage: "dev", author: "engineer",
|
||||
updated_at: "2026-08-04T09:00:00Z" }
|
||||
];
|
||||
}
|
||||
function getModels() {
|
||||
var m = load(MODELS_KEY, null);
|
||||
if (!m) { m = seedModels(); save(MODELS_KEY, m); }
|
||||
return m;
|
||||
}
|
||||
function renderModels() {
|
||||
var tbody = document.getElementById("model-tbody");
|
||||
tbody.innerHTML = "";
|
||||
getModels().forEach(function (m, i) {
|
||||
var tr = el("tr");
|
||||
tr.appendChild(el("td", null, m.name));
|
||||
tr.appendChild(el("td", null, m.version));
|
||||
var tdStage = el("td");
|
||||
tdStage.appendChild(el("span", "stage stage-" + m.stage, m.stage));
|
||||
tr.appendChild(tdStage);
|
||||
tr.appendChild(el("td", null, m.author));
|
||||
tr.appendChild(el("td", null, (m.updated_at || "").slice(0, 10)));
|
||||
var ops = el("td");
|
||||
var idx = STAGES.indexOf(m.stage);
|
||||
if (idx < STAGES.length - 1) {
|
||||
var btn = el("button", null, "提升到 " + STAGES[idx + 1]);
|
||||
btn.onclick = function () { promoteModel(i); };
|
||||
ops.appendChild(btn);
|
||||
} else {
|
||||
ops.appendChild(el("span", "hint", "已达 prod"));
|
||||
}
|
||||
if (idx > 0) {
|
||||
var rb = el("button", null, "回滚到 " + STAGES[idx - 1]);
|
||||
rb.style.marginLeft = "6px";
|
||||
rb.onclick = function () { rollbackModel(i); };
|
||||
ops.appendChild(rb);
|
||||
}
|
||||
tr.appendChild(ops);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
function registerModel() {
|
||||
var name = document.getElementById("model-name").value.trim();
|
||||
var version = document.getElementById("model-version").value.trim();
|
||||
if (!name || !SEMVER_RE.test(version)) { alert("模型名必填,版本须为 semver(如 1.0.0)"); return; }
|
||||
var models = getModels();
|
||||
var existing = models.filter(function (m) { return m.name === name; });
|
||||
if (existing.length) {
|
||||
var latest = existing.reduce(function (a, b) {
|
||||
return semverKey(a.version) > semverKey(b.version) ? a : b;
|
||||
});
|
||||
if (semverKey(version) <= semverKey(latest.version)) {
|
||||
alert("版本必须大于当前最新 " + latest.version); return;
|
||||
}
|
||||
}
|
||||
models.push({ name: name, version: version, stage: "dev",
|
||||
author: currentUserName, updated_at: new Date().toISOString() });
|
||||
save(MODELS_KEY, models);
|
||||
audit("create", "model", name + "@" + version + " 注册(dev)");
|
||||
renderModels();
|
||||
}
|
||||
function promoteModel(i) {
|
||||
var models = getModels();
|
||||
var m = models[i];
|
||||
var next = STAGES[STAGES.indexOf(m.stage) + 1];
|
||||
if (!next) return;
|
||||
m.stage = next; m.updated_at = new Date().toISOString();
|
||||
save(MODELS_KEY, models);
|
||||
audit("promote", "model", m.name + "@" + m.version + " → " + next);
|
||||
renderModels();
|
||||
}
|
||||
function rollbackModel(i) {
|
||||
var models = getModels();
|
||||
var m = models[i];
|
||||
var prev = STAGES[STAGES.indexOf(m.stage) - 1];
|
||||
if (!prev) return;
|
||||
m.stage = prev; m.updated_at = new Date().toISOString();
|
||||
save(MODELS_KEY, models);
|
||||
audit("rollback", "model", m.name + "@" + m.version + " → " + prev);
|
||||
renderModels();
|
||||
}
|
||||
|
||||
/* ---------------- 知识库管理 ---------------- */
|
||||
function getKbDocs() { return load(KB_KEY, []); }
|
||||
function renderKb() {
|
||||
var tbody = document.getElementById("kb-tbody");
|
||||
tbody.innerHTML = "";
|
||||
getKbDocs().forEach(function (d, i) {
|
||||
var tr = el("tr");
|
||||
tr.appendChild(el("td", null, d.title));
|
||||
tr.appendChild(el("td", null, d.source));
|
||||
tr.appendChild(el("td", null, String(d.sections.length)));
|
||||
tr.appendChild(el("td", "idx-ok", d.status || "已索引"));
|
||||
var ops = el("td");
|
||||
var view = el("button", null, "查看引用");
|
||||
view.onclick = function () {
|
||||
var box = document.getElementById("kb-detail");
|
||||
box.hidden = false;
|
||||
box.innerHTML = "";
|
||||
box.appendChild(el("div", null, "《" + d.title + "》 " + d.source));
|
||||
d.sections.forEach(function (s) {
|
||||
box.appendChild(el("div", null, "§ " + s.heading + " — " + s.snippet));
|
||||
});
|
||||
};
|
||||
ops.appendChild(view);
|
||||
tr.appendChild(ops);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
function loadKbFromCitations() {
|
||||
// 首选:web/chat/citations.json(模板知识库编译产物,含真实来源路径)
|
||||
fetch("../chat/citations.json").then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (d && d.documents && !getKbDocs().length) {
|
||||
save(KB_KEY, d.documents.map(function (doc) {
|
||||
return { title: doc.title, source: doc.source,
|
||||
sections: doc.sections, status: "已索引" };
|
||||
}));
|
||||
}
|
||||
renderKb();
|
||||
}).catch(function () { renderKb(); });
|
||||
}
|
||||
function uploadKb() {
|
||||
var f = document.getElementById("kb-file").files[0];
|
||||
if (!f) { alert("请选择 .md/.txt 文档"); return; }
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
var text = String(reader.result || "");
|
||||
var titleM = text.match(/^#\s+(.+)$/m);
|
||||
var docs = getKbDocs();
|
||||
docs.push({
|
||||
title: titleM ? titleM[1].trim() : f.name.replace(/\.\w+$/, ""),
|
||||
source: "upload/" + f.name,
|
||||
sections: [{ heading: "全文", snippet: text.replace(/\s+/g, " ").slice(0, 160) }],
|
||||
status: "已索引(本地)"
|
||||
});
|
||||
save(KB_KEY, docs);
|
||||
audit("create", "kb_doc", f.name + " 上传并索引");
|
||||
document.getElementById("kb-hint").textContent = f.name + " 已索引";
|
||||
renderKb();
|
||||
};
|
||||
reader.readAsText(f, "utf-8");
|
||||
}
|
||||
|
||||
/* ---------------- 告警确认(PRD 8.1 状态机) ---------------- */
|
||||
var ALERT_STATUS = { pending: "待处理", acked: "已确认", closed: "已闭环" };
|
||||
function seedAlerts() {
|
||||
var now = Date.now();
|
||||
return [
|
||||
{ id: "ALM-2001", severity: "P0", point: "CLF-01.TEMP",
|
||||
message: "氯化炉温度骤升越上限(920℃ > 900℃)",
|
||||
time: new Date(now - 12 * 60000).toLocaleString("zh-CN"), status: "pending",
|
||||
sop: "1) 现场确认热电偶;2) 降氯气进料 10%;3) 通知值班长;4) 必要时切旁路。" },
|
||||
{ id: "ALM-2002", severity: "P1", point: "RF-01.VAC",
|
||||
message: "还原炉真空度波动超阈值",
|
||||
time: new Date(now - 45 * 60000).toLocaleString("zh-CN"), status: "pending",
|
||||
sop: "1) 检查真空泵组;2) 核查泄漏点;3) 加强监控 30 分钟。" },
|
||||
{ id: "ALM-2003", severity: "P2", point: "ST-01.STEAM",
|
||||
message: "蒸汽流量低于经验区间",
|
||||
time: new Date(now - 90 * 60000).toLocaleString("zh-CN"), status: "acked",
|
||||
sop: "记录趋势,纳入班报跟踪。" }
|
||||
];
|
||||
}
|
||||
function getAlerts() {
|
||||
var a = load(ALERTS_KEY, null);
|
||||
if (!a) { a = seedAlerts(); save(ALERTS_KEY, a); }
|
||||
return a;
|
||||
}
|
||||
function renderAlerts() {
|
||||
var filter = document.getElementById("alert-filter").value;
|
||||
var box = document.getElementById("alert-list");
|
||||
box.innerHTML = "";
|
||||
var rank = { P0: 3, P1: 2, P2: 1 };
|
||||
getAlerts()
|
||||
.filter(function (a) { return !filter || a.status === filter; })
|
||||
.sort(function (a, b) { return rank[b.severity] - rank[a.severity]; })
|
||||
.forEach(function (a) {
|
||||
var st = severityStyles[a.severity] || {};
|
||||
var item = el("div", "alert-item");
|
||||
item.style.color = st.fg || "inherit";
|
||||
item.style.background = st.bg || "transparent";
|
||||
if (st.border) item.style.borderLeftColor = st.border;
|
||||
item.appendChild(el("span", "sev", a.severity));
|
||||
item.appendChild(el("span", null, a.id + " · " + a.message));
|
||||
item.appendChild(el("span", "st", ALERT_STATUS[a.status]));
|
||||
item.onclick = function () { renderAlertDetail(a.id); };
|
||||
box.appendChild(item);
|
||||
});
|
||||
}
|
||||
function renderAlertDetail(id) {
|
||||
var a = getAlerts().find(function (x) { return x.id === id; });
|
||||
var box = document.getElementById("alert-detail");
|
||||
if (!a) return;
|
||||
box.innerHTML = "";
|
||||
box.appendChild(el("div", null, a.id + " · " + a.severity + " · " + a.point));
|
||||
box.appendChild(el("div", "hint", a.message + " · " + a.time + " · 状态:" + ALERT_STATUS[a.status]));
|
||||
var llm = el("div", "llm", "点击「生成 LLM 解释」获取可能原因与处置建议(PRD 场景A)");
|
||||
box.appendChild(llm);
|
||||
var btnLlm = el("button", "primary", "生成 LLM 解释");
|
||||
btnLlm.onclick = function () {
|
||||
btnLlm.disabled = true;
|
||||
llm.textContent = "生成中…";
|
||||
fetch(INFER_BASE + "/v1/chat/completions", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "iaop-ti-cl4-v1", max_tokens: 512, messages: [
|
||||
{ role: "system", content: "你是工业报警解释助手,输出【可能原因】与【处置建议】。" },
|
||||
{ role: "user", content: "告警 " + a.severity + " 点位 " + a.point + ":" + a.message }
|
||||
] })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
llm.textContent = (d.choices && d.choices[0] && d.choices[0].message.content) || "(空响应)";
|
||||
})
|
||||
.catch(function (e) { llm.textContent = "解释生成失败:" + e.message; })
|
||||
.finally(function () { btnLlm.disabled = false; });
|
||||
};
|
||||
box.appendChild(btnLlm);
|
||||
box.appendChild(el("div", "sop", "SOP:" + a.sop));
|
||||
// 状态流转按钮(P0 高利害:必须先确认再闭环,PRD 8.1)
|
||||
var row = el("div", "row");
|
||||
if (a.status === "pending") {
|
||||
var ack = el("button", "primary", "确认告警");
|
||||
ack.onclick = function () { transitionAlert(a.id, "acked"); };
|
||||
row.appendChild(ack);
|
||||
if (a.severity === "P0") row.appendChild(el("span", "hint", "P0 高利害:确认后仍需人工复核闭环"));
|
||||
}
|
||||
if (a.status === "acked") {
|
||||
var close = el("button", "primary", "闭环");
|
||||
close.onclick = function () { transitionAlert(a.id, "closed"); };
|
||||
row.appendChild(close);
|
||||
}
|
||||
box.appendChild(row);
|
||||
}
|
||||
function transitionAlert(id, to) {
|
||||
var alerts = getAlerts();
|
||||
var a = alerts.find(function (x) { return x.id === id; });
|
||||
if (!a) return;
|
||||
a.status = to;
|
||||
save(ALERTS_KEY, alerts);
|
||||
audit(to === "acked" ? "ack" : "close", "alert", a.id + " " + a.severity + " → " + ALERT_STATUS[to]);
|
||||
renderAlerts(); renderAlertDetail(id);
|
||||
}
|
||||
|
||||
/* ---------------- 审计查询 ---------------- */
|
||||
function renderAudit() {
|
||||
var q = document.getElementById("audit-q").value.trim();
|
||||
var action = document.getElementById("audit-action").value;
|
||||
var tbody = document.getElementById("audit-tbody");
|
||||
tbody.innerHTML = "";
|
||||
var log = (typeof UserStore !== "undefined") ? UserStore.auditLog() : [];
|
||||
log.slice().reverse()
|
||||
.filter(function (e) {
|
||||
if (action && e.action !== action) return false;
|
||||
if (q && (e.actor + e.resource + e.reason).indexOf(q) < 0) return false;
|
||||
return true;
|
||||
})
|
||||
.forEach(function (e) {
|
||||
var tr = el("tr");
|
||||
tr.appendChild(el("td", null, e.time.slice(0, 19).replace("T", " ")));
|
||||
tr.appendChild(el("td", null, e.actor));
|
||||
tr.appendChild(el("td", null, e.action));
|
||||
tr.appendChild(el("td", null, e.resource));
|
||||
tr.appendChild(el("td", null, e.reason));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 启动 ---------------- */
|
||||
(function init() {
|
||||
function boot(user) {
|
||||
if (user) {
|
||||
currentUserName = user.username;
|
||||
document.getElementById("who").textContent = user.username + " · " + user.role;
|
||||
}
|
||||
document.querySelectorAll(".tab").forEach(function (tab) {
|
||||
tab.onclick = function () {
|
||||
document.querySelectorAll(".tab").forEach(function (t) { t.classList.remove("active"); });
|
||||
document.querySelectorAll(".page").forEach(function (p) { p.classList.remove("active"); });
|
||||
tab.classList.add("active");
|
||||
document.getElementById("page-" + tab.dataset.page).classList.add("active");
|
||||
};
|
||||
});
|
||||
document.getElementById("btn-register").onclick = registerModel;
|
||||
document.getElementById("btn-kb-upload").onclick = uploadKb;
|
||||
document.getElementById("alert-filter").onchange = renderAlerts;
|
||||
document.getElementById("btn-audit-filter").onclick = renderAudit;
|
||||
document.getElementById("btn-audit-export").onclick = function () {
|
||||
download("audit-export-" + Date.now() + ".json",
|
||||
JSON.stringify(typeof UserStore !== "undefined" ? UserStore.auditLog() : [], null, 2));
|
||||
};
|
||||
// severity 配色资产(与驾驶舱同源;取不到用内置兜底)
|
||||
fetch("../cockpit/plans/alarm_panel.ti.json").then(function (r) {
|
||||
return r.ok ? r.json() : null;
|
||||
}).then(function (p) {
|
||||
if (p && p.severityStyles) { severityStyles = p.severityStyles; renderAlerts(); }
|
||||
}).catch(function () {});
|
||||
renderModels(); loadKbFromCitations(); renderAlerts(); renderAudit();
|
||||
}
|
||||
// 认证守卫(#150 IAOP_AUTH,在则强制登录;纯静态独立运行时降级免登录演示)
|
||||
if (typeof IAOP_AUTH !== "undefined") {
|
||||
IAOP_AUTH.requireLoginElseRedirect("../auth/login.html").then(function (user) {
|
||||
if (user) boot(user);
|
||||
});
|
||||
} else {
|
||||
boot(null);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 管理控制台</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- iAOP 管理功能界面(issue #135 / PRD 8.1/8.2)
|
||||
纯静态单页(无构建链):模型管理 / 知识库管理 / 告警确认 / 审计查询。
|
||||
语义对齐后端内核:template_registry(dev/staging/prod 逐级 promote + 回滚)、
|
||||
rag-kb 文档资产、PRD 8.1 告警状态机(待处理→已确认→已闭环)、_log 审计。 -->
|
||||
<header id="topbar">
|
||||
<h1>iAOP 管理控制台 <span class="sub">模型 / 知识库 / 告警 / 审计</span></h1>
|
||||
<div id="topbar-right">
|
||||
<span id="who" class="hint"></span>
|
||||
<a href="../cockpit/index.html">驾驶舱</a>
|
||||
<a href="../studio/index.html">配置台</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav id="tabs">
|
||||
<button class="tab active" data-page="models">模型管理</button>
|
||||
<button class="tab" data-page="kb">知识库管理</button>
|
||||
<button class="tab" data-page="alerts">告警确认</button>
|
||||
<button class="tab" data-page="audit">审计查询</button>
|
||||
</nav>
|
||||
|
||||
<!-- ======================= 模型管理 ======================= -->
|
||||
<section class="page active" id="page-models">
|
||||
<div class="panel">
|
||||
<h2>模型模板列表(阶段:dev → staging → prod,对齐 template_registry)</h2>
|
||||
<div class="row">
|
||||
<input id="model-name" placeholder="模型名,如 quality_forecast">
|
||||
<input id="model-version" placeholder="初始版本,如 1.0.0">
|
||||
<button id="btn-register" class="primary">注册新版本</button>
|
||||
<span class="hint">语义对齐 core/model-framework/template_registry.py:promote 逐级提升、rollback 回退、全程审计</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>模型</th><th>版本</th><th>阶段</th><th>作者</th><th>更新时间</th><th>操作</th></tr></thead>
|
||||
<tbody id="model-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= 知识库管理 ======================= -->
|
||||
<section class="page" id="page-kb">
|
||||
<div class="panel">
|
||||
<h2>知识库文档(来源:templates/*/rag-kb/documents,经 build_citations 索引)</h2>
|
||||
<div class="row">
|
||||
<input type="file" id="kb-file" accept=".md,.txt">
|
||||
<button id="btn-kb-upload" class="primary">上传并索引</button>
|
||||
<span id="kb-hint" class="hint"></span>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>文档</th><th>来源</th><th>章节数</th><th>索引状态</th><th>操作</th></tr></thead>
|
||||
<tbody id="kb-tbody"></tbody>
|
||||
</table>
|
||||
<div id="kb-detail" class="detail" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= 告警确认 ======================= -->
|
||||
<section class="page" id="page-alerts">
|
||||
<div class="split-alerts">
|
||||
<div class="panel">
|
||||
<h2>告警列表(状态机:待处理 → 已确认 → 已闭环,PRD 8.1)</h2>
|
||||
<div class="row">
|
||||
<select id="alert-filter">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待处理</option>
|
||||
<option value="acked">已确认</option>
|
||||
<option value="closed">已闭环</option>
|
||||
</select>
|
||||
<span class="hint">severity 配色来自 alarm_panel.ti.yaml(P0 红 / P1 黄 / P2 蓝)</span>
|
||||
</div>
|
||||
<div id="alert-list"></div>
|
||||
</div>
|
||||
<div class="panel" id="alert-detail-panel">
|
||||
<h2>告警详情(LLM 解释 + SOP)</h2>
|
||||
<div id="alert-detail"><p class="hint">选择左侧告警查看详情</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================= 审计查询 ======================= -->
|
||||
<section class="page" id="page-audit">
|
||||
<div class="panel">
|
||||
<h2>操作审计日志</h2>
|
||||
<div class="row">
|
||||
<input id="audit-q" placeholder="关键词(操作者/资源/原因)">
|
||||
<select id="audit-action">
|
||||
<option value="">全部动作</option>
|
||||
<option value="create">create</option>
|
||||
<option value="update">update</option>
|
||||
<option value="delete">delete</option>
|
||||
<option value="publish">publish</option>
|
||||
<option value="rollback">rollback</option>
|
||||
<option value="promote">promote</option>
|
||||
<option value="ack">ack</option>
|
||||
<option value="close">close</option>
|
||||
<option value="login">login</option>
|
||||
</select>
|
||||
<button id="btn-audit-filter">筛选</button>
|
||||
<button id="btn-audit-export" class="primary">导出 JSON</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>时间</th><th>操作者</th><th>动作</th><th>资源</th><th>结果/原因</th></tr></thead>
|
||||
<tbody id="audit-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="../auth/auth.js"></script>
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user