feat: add monitoring settings tab, workbench view, and inspection alert improvements
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

This commit is contained in:
2026-10-30 12:12:04 +08:00
committed by trae-solo-agent
parent 16dd04f746
commit 4a02370006
26 changed files with 2569 additions and 233 deletions

View File

@@ -2,6 +2,8 @@
export const STORAGE_KEY_PRODUCT_NAME = 'netcatty_product_name_v1';
/** User-editable topology monitoring dashboard title. */
export const STORAGE_KEY_MONITORING_DASHBOARD_TITLE = 'netcatty_monitoring_dashboard_title_v1';
/** User-selected topology map background filename (relative to userData/monitoring/maps). */
export const STORAGE_KEY_MONITORING_MAP_BG_SELECTED = 'netcatty_monitoring_map_bg_selected_v1';
/** User-editable product name shown in inspection PDF reports. */
export const STORAGE_KEY_REPORT_PRODUCT_NAME = 'netcatty_report_product_name_v1';

View File

@@ -138,31 +138,44 @@ const REMOTE_API_MAP: Record<string, { method: string; path: string; arrayField?
getLatestAll: { method: "GET", path: "/api/inspection/results", arrayField: "results" },
getLatestByDevice: { method: "GET", path: "/api/inspection/results", arrayField: "results" },
getRecentResults: { method: "GET", path: "/api/inspection/results/recent", arrayField: "results" },
getResultsByTimeRange: { method: "GET", path: "/api/inspection/results/range", arrayField: "results" },
getActiveAlerts: { method: "GET", path: "/api/alerts", arrayField: "alerts" },
listAlerts: { method: "GET", path: "/api/alerts", arrayField: "alerts" },
getAlertStats: { method: "GET", path: "/api/alert-stats" },
listRules: { method: "GET", path: "/api/alert-rules", arrayField: "rules" },
getRule: { method: "GET", path: "/api/alert-rules", arrayField: "rules" },
createRule: { method: "POST", path: "/api/alert-rule" },
updateRule: { method: "POST", path: "/api/alert-rule" },
deleteRule: { method: "POST", path: "/api/alert-rule" },
toggleRule: { method: "POST", path: "/api/alert-rule" },
getAllExtensions: { method: "GET", path: "/api/devices", arrayField: "devices" },
getEnabledDevices: { method: "GET", path: "/api/devices/enabled", arrayField: "devices" },
schedulerStatus: { method: "GET", path: "/api/inspection/scheduler-status" },
getCatalog: { method: "GET", path: "/api/inspection/catalog", arrayField: "catalog" },
getSysConfig: { method: "GET", path: "/api/inspection/sys-config" },
updateSysConfig: { method: "POST", path: "/api/inspection/sys-config" },
getLabels: { method: "GET", path: "/api/inspection/labels" },
listReportSnapshots: { method: "GET", path: "/api/report/snapshots", arrayField: "snapshots" },
isReportSnapshotRunning: { method: "GET", path: "/api/report/snapshot-status" },
getReportSchedule: { method: "GET", path: "/api/report/schedule" },
saveReportSchedule: { method: "POST", path: "/api/report/schedule" },
toggleReportScheduler: { method: "POST", path: "/api/report/scheduler-toggle" },
reportSchedulerStatus: { method: "GET", path: "/api/report/scheduler-status" },
listDailyReports: { method: "GET", path: "/api/daily-reports", arrayField: "reports" },
batchSetInspectionEnabled: { method: "POST", path: "/api/devices/batch-enable" },
runReportAiSummary: { method: "POST", path: "/api/run-report-ai-summary" },
generateDailyReport: { method: "POST", path: "/api/generate-daily-report" },
acknowledgeAlert: { method: "POST", path: "/api/alert/__ID__/acknowledge" },
resolveAlert: { method: "POST", path: "/api/alert/__ID__/resolve" },
closeAlert: { method: "POST", path: "/api/alert/__ID__/close" },
updateAlertNote: { method: "POST", path: "/api/alert/__ID__/note" },
};
// 某些 API 返回数组 —— 在 REMOTE_API_MAP 里用 arrayField 标记
// 找不到映射的 API 默认返回 null,让调用方自己兜底
const ARRAY_METHODS = new Set([
"listReportSnapshots", "listDailyReports", "listRules", "getLatestAll",
"getLatestByDevice", "getRecentResults", "getActiveAlerts", "listAlerts",
"getLatestByDevice", "getRecentResults", "getResultsByTimeRange", "getActiveAlerts", "listAlerts",
"getAllExtensions", "getEnabledDevices", "getCatalog",
]);
@@ -178,10 +191,19 @@ async function remoteFetch(name: string, ...args: any[]): Promise<any> {
if (name === "schedulerStatus" || name === "isReportSnapshotRunning") return { running: false };
return null;
}
let url = `${mapping.path}?token=${encodeURIComponent(REMOTE_TOKEN)}`;
let url = mapping.path;
// 告警状态变更:把 __ID__ 替换成实际 alertId
if (url.includes("__ID__") && args[0]) {
url = url.replace("__ID__", encodeURIComponent(args[0]));
args = args.slice(1); // alertId 从参数里移除,剩下的是 payload
}
url += `?token=${encodeURIComponent(REMOTE_TOKEN)}`;
if (name === "getLatestByDevice" && args[0]) {
url += `&device_id=${encodeURIComponent(args[0])}`;
}
if (name === "getResultsByTimeRange" && args[0] !== undefined && args[1] !== undefined) {
url += `&start=${encodeURIComponent(args[0])}&end=${encodeURIComponent(args[1])}`;
}
if ((name === "listAlerts" || name === "listReportSnapshots") && args[0] !== undefined) {
const opts = typeof args[0] === "object" ? args[0] : { limit: args[0] };
if (opts.status) url += `&status=${opts.status}`;
@@ -204,6 +226,24 @@ async function remoteFetch(name: string, ...args: any[]): Promise<any> {
} else if (name === "generateDailyReport") {
// invoke("generateDailyReport", options)
fetchOpts.body = JSON.stringify(args[0] || {});
} else if (name === "createRule") {
fetchOpts.body = JSON.stringify({ action: "create", data: args[0] || {} });
} else if (name === "updateRule") {
fetchOpts.body = JSON.stringify({ action: "update", id: args[0], updates: args[1] || {} });
} else if (name === "deleteRule") {
fetchOpts.body = JSON.stringify({ action: "delete", id: args[0] });
} else if (name === "toggleRule") {
fetchOpts.body = JSON.stringify({ action: "toggle", id: args[0], enabled: !!args[1] });
} else if (name === "toggleReportScheduler") {
fetchOpts.body = JSON.stringify({ enabled: !!args[0] });
} else if (name === "acknowledgeAlert") {
fetchOpts.body = JSON.stringify({ operator: args[0] || "remote" });
} else if (name === "resolveAlert") {
fetchOpts.body = JSON.stringify({ reason: args[0] || "manual", operator: args[1] || "remote" });
} else if (name === "closeAlert") {
fetchOpts.body = JSON.stringify({ note: args[0] || "" });
} else if (name === "updateAlertNote") {
fetchOpts.body = JSON.stringify({ note: args[0] || "" });
} else {
fetchOpts.body = JSON.stringify(args[0] !== undefined ? args[0] : {});
}
@@ -213,27 +253,27 @@ async function remoteFetch(name: string, ...args: any[]): Promise<any> {
// 适配不同返回结构
if (data && typeof data === "object") {
if (Array.isArray(data)) return data;
// 优先用 arrayField 约定的字段
// getAllExtensions:必须把 devices 数组转成 Record<host_id, ext>
if (name === "getAllExtensions" && Array.isArray(data.devices)) {
const map: Record<string, any> = {};
for (const d of data.devices) {
if (d?.host_id) map[d.host_id] = d;
}
return map;
}
// reportSchedulerStatus: { running, schedule }
if (name === "reportSchedulerStatus" && "running" in data) return data;
// 优先用 arrayField 约定的字段(getAllExtensions 已在上文处理 Record 转换)
if (mapping.arrayField && Array.isArray(data[mapping.arrayField])) {
return data[mapping.arrayField];
}
if (data.alerts !== undefined) return data.alerts;
if (data.devices !== undefined) {
if (name === "getAllExtensions") {
// getAllExtensions 返回 Record<string, DeviceExtension>
const map: Record<string, any> = {};
for (const d of data.devices) {
map[d.host_id] = d;
}
return map;
}
return data.devices;
}
if (data.devices !== undefined) return data.devices;
if (data.rules !== undefined) return data.rules;
if (data.results !== undefined) return data.results;
if (data.snapshots !== undefined) return data.snapshots;
if (data.reports !== undefined) return data.reports;
if (data.schedule !== undefined) return data.schedule;
if (data.schedule !== undefined && (name === "getReportSchedule" || name === "saveReportSchedule")) return data.schedule;
if (data.ok === true && data.data !== undefined) return data.data;
if ("running" in data) return data; // schedulerStatus / snapshot-status
return data;
@@ -315,6 +355,8 @@ export const inspectionResultApi = {
invoke("getLatestByDevice", hostId, limit),
getRecent: (limit = 50): Promise<InspectionResult[]> =>
invoke("getRecentResults", limit),
getByTimeRange: (startTime: number, endTime: number): Promise<InspectionResult[]> =>
invoke("getResultsByTimeRange", startTime, endTime),
};
// ---------- 告警记录 ----------

View File

@@ -9,6 +9,10 @@ const safeParse = <T>(value: string | null): T | null => {
export const LOCAL_STORAGE_ADAPTER_CHANGED_EVENT = 'netcatty:local-storage-adapter-changed';
// 全局钩子:写后调用。AppSideEffects 注册后用来同步权威源。
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = null as null | ((key: string, value: string | null) => void);
const pendingChangedKeys = new Set<string>();
let emitChangedKeysTimer: ReturnType<typeof setTimeout> | null = null;
@@ -55,6 +59,8 @@ function safeSetItem(key: string, value: string): boolean {
}
localStorage.setItem(key, value);
emitLocalStorageAdapterChanged(key);
// 同步到权威源(如果钩子已注册)
try { (globalThis as any).__NETCATTY_SETTINGS_SYNC__?.(key, value); } catch {}
return true;
} catch (err) {
if (
@@ -107,5 +113,7 @@ export const localStorageAdapter = {
if (localStorage.getItem(key) === null) return;
localStorage.removeItem(key);
emitLocalStorageAdapterChanged(key);
// 同步到权威源(删除)
try { (globalThis as any).__NETCATTY_SETTINGS_SYNC__?.(key, null); } catch {}
},
};