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
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:
@@ -88,7 +88,7 @@ const STATUS_COLORS: Record<AlertRecordStatus, string> = {
|
||||
closed: "text-muted-foreground",
|
||||
};
|
||||
|
||||
export function AlertCenterPage() {
|
||||
export function AlertCenterPage({ initialAlertId }: { initialAlertId?: string }) {
|
||||
const [alerts, setAlerts] = useState<AlertRecord[]>([]);
|
||||
const [stats, setStats] = useState<AlertStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -97,6 +97,19 @@ export function AlertCenterPage() {
|
||||
const [noteDialogOpen, setNoteDialogOpen] = useState(false);
|
||||
const [noteText, setNoteText] = useState("");
|
||||
|
||||
// 从工作台跳转时自动打开指定告警的详情弹窗
|
||||
const autoOpenedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!initialAlertId || autoOpenedRef.current) return;
|
||||
if (alerts.length === 0) return; // 等数据加载完
|
||||
const target = alerts.find((a) => a.id === initialAlertId);
|
||||
if (target) {
|
||||
autoOpenedRef.current = true;
|
||||
setSelectedAlert(target);
|
||||
setDetailOpen(true);
|
||||
}
|
||||
}, [initialAlertId, alerts]);
|
||||
|
||||
// 防御:清洗告警里可能残留的 [object Object](老数据修复前写入的坏值)
|
||||
const _clean = (v: any): string => {
|
||||
if (v === null || v === undefined) return "";
|
||||
|
||||
@@ -80,35 +80,55 @@ function formatDuration(ms: number) {
|
||||
/** 从 vault 解密 + device extension 拿到所有设备的 SSH target
|
||||
* enabledDeviceIds 为空/undefined = 全部设备;有值则只返回列表内的
|
||||
* 返回值第二个元素是完整 host 列表(用于 SchedulePanel 渲染设备多选)
|
||||
*
|
||||
* 远程浏览器模式下:从 HTTP /api/vault/hosts 读 hosts(已脱敏),
|
||||
* 从 /api/devices 读 catalog_id。targets 不带真实凭据(远程跑不了 SSH)。
|
||||
*/
|
||||
async function buildAllDeviceTargets(enabledDeviceIds?: string[]): Promise<{
|
||||
targets: ReportDeviceTarget[];
|
||||
allHosts: Array<{ id: string; hostname: string; catalog_id?: string; label: string }>;
|
||||
}> {
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
const REMOTE_TOKEN = (window as any).__REMOTE_TOKEN__ || "";
|
||||
|
||||
const targets: ReportDeviceTarget[] = [];
|
||||
const allHosts: Array<{ id: string; hostname: string; catalog_id?: string; label: string }> = [];
|
||||
|
||||
let hosts: HostAny[] = [];
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_HOSTS);
|
||||
if (raw) hosts = await decryptHosts(JSON.parse(raw));
|
||||
} catch (_) {}
|
||||
|
||||
let keys: KeyAny[] = [];
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_KEYS);
|
||||
if (raw) keys = await decryptKeys(JSON.parse(raw));
|
||||
} catch (_) {}
|
||||
|
||||
let hosts: any[] = [];
|
||||
let extensions: Record<string, any> = {};
|
||||
try {
|
||||
const raw = localStorage.getItem("netcatty_inspection_device_extension_v1");
|
||||
if (raw) extensions = JSON.parse(raw);
|
||||
} catch (_) {}
|
||||
|
||||
if (IS_REMOTE) {
|
||||
// ── 远程浏览器:从 HTTP API 读 ──
|
||||
try {
|
||||
const url = `/api/vault/hosts?token=${encodeURIComponent(REMOTE_TOKEN)}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
hosts = Array.isArray(data?.data) ? data.data : [];
|
||||
} catch (_) {}
|
||||
// catalog_id 从 /api/devices 读
|
||||
try {
|
||||
const url = `/api/devices?token=${encodeURIComponent(REMOTE_TOKEN)}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
for (const d of (data?.devices || [])) {
|
||||
if (d?.host_id) extensions[d.host_id] = { catalog_id: d.catalog_id || d.catalog_type };
|
||||
}
|
||||
} catch (_) {}
|
||||
} else {
|
||||
// ── Electron 本地:从 localStorage 读 ──
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_HOSTS);
|
||||
if (raw) hosts = await decryptHosts(JSON.parse(raw));
|
||||
} catch (_) {}
|
||||
try {
|
||||
const raw = localStorage.getItem("netcatty_inspection_device_extension_v1");
|
||||
if (raw) extensions = JSON.parse(raw);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
for (const host of hosts) {
|
||||
const ext = extensions[host.id];
|
||||
const catalogId = ext?.catalog_id || undefined;
|
||||
const catalogId = ext?.catalog_id || host.catalog_id || undefined;
|
||||
const label = host.label || host.hostname;
|
||||
|
||||
allHosts.push({ id: host.id, hostname: host.hostname, catalog_id: catalogId, label });
|
||||
@@ -116,18 +136,14 @@ async function buildAllDeviceTargets(enabledDeviceIds?: string[]): Promise<{
|
||||
// 过滤:enabledDeviceIds 有值且当前不在列表里 → 跳过
|
||||
if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue;
|
||||
|
||||
const key = host.identityFileId
|
||||
? keys.find((k: KeyAny) => k.id === host.identityFileId)
|
||||
: undefined;
|
||||
|
||||
targets.push({
|
||||
device_id: host.id,
|
||||
hostname: host.hostname,
|
||||
port: host.port || 22,
|
||||
username: host.username,
|
||||
username: host.username || "",
|
||||
password: host.password,
|
||||
privateKeyContent: key?.privateKeyContent,
|
||||
passphrase: key?.passphrase,
|
||||
privateKeyContent: host.privateKeyContent || undefined,
|
||||
passphrase: host.passphrase,
|
||||
catalog_id: catalogId,
|
||||
});
|
||||
}
|
||||
@@ -1237,23 +1253,35 @@ function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: {
|
||||
}) {
|
||||
const [local, setLocal] = useState<ReportSchedule>(schedule);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// dirty 保护:用户在面板里编辑过 → 不要被外部 props.schedule 变化静默覆盖
|
||||
// (比如主进程广播 schedule 变更、其他组件触发 loadSchedule 等)
|
||||
const dirtyRef = useRef(false);
|
||||
|
||||
useEffect(() => { setLocal(schedule); }, [schedule]);
|
||||
useEffect(() => {
|
||||
if (!dirtyRef.current) setLocal(schedule);
|
||||
}, [schedule]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
await onSave(local);
|
||||
dirtyRef.current = false;
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
/** 编辑点统一入口:改 local + 标记 dirty,防止外部 props 覆盖未保存的本地编辑 */
|
||||
const edit = useCallback((updater: ReportSchedule | ((p: ReportSchedule) => ReportSchedule)) => {
|
||||
dirtyRef.current = true;
|
||||
setLocal(updater);
|
||||
}, []);
|
||||
|
||||
// 设备多选 helpers
|
||||
// enabledDeviceIds 约定:null/undefined = 全部,[] = 一个都不选,[id1,id2] = 指定
|
||||
const selectedAll = !local.enabledDeviceIds; // null / undefined → 全部
|
||||
const toggleAllDevices = (selectAll: boolean) => {
|
||||
setLocal(p => ({ ...p, enabledDeviceIds: selectAll ? null : [] }));
|
||||
edit(p => ({ ...p, enabledDeviceIds: selectAll ? null : [] }));
|
||||
};
|
||||
const toggleDevice = (deviceId: string) => {
|
||||
setLocal(p => {
|
||||
edit(p => {
|
||||
const cur = p.enabledDeviceIds;
|
||||
if (!cur) {
|
||||
// 当前是"全部" —— 取消这一台变成"除了这台"
|
||||
@@ -1291,7 +1319,11 @@ function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: {
|
||||
<div className="text-xs text-muted-foreground">按周期自动巡检</div>
|
||||
</div>
|
||||
<Switch checked={local.enabled}
|
||||
onCheckedChange={(v) => { setLocal(p => ({ ...p, enabled: v })); onToggleScheduler(v); }} />
|
||||
onCheckedChange={(v) => {
|
||||
edit(p => ({ ...p, enabled: v }));
|
||||
// onToggleScheduler 直接写主进程,成功后清 dirty(async)
|
||||
Promise.resolve(onToggleScheduler(v)).then(() => { dirtyRef.current = false; });
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* 巡检周期 */}
|
||||
@@ -1300,7 +1332,7 @@ function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: {
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="number" min={1} max={72} step={1} className="w-20 rounded border border-border/60 bg-background px-2 py-1 text-sm"
|
||||
value={local.defaultIntervalHours}
|
||||
onChange={(e) => setLocal(p => ({ ...p, defaultIntervalHours: Number(e.target.value) || 1 }))} />
|
||||
onChange={(e) => edit(p => ({ ...p, defaultIntervalHours: Number(e.target.value) || 1 }))} />
|
||||
<span className="text-xs text-muted-foreground">小时</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1310,7 +1342,7 @@ function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: {
|
||||
<div className="mb-1 text-sm font-medium">日报告触发时间</div>
|
||||
<input type="time" className="w-full rounded border border-border/60 bg-background px-2 py-1 text-sm"
|
||||
value={local.dailyReportTime}
|
||||
onChange={(e) => setLocal(p => ({ ...p, dailyReportTime: e.target.value }))} />
|
||||
onChange={(e) => edit(p => ({ ...p, dailyReportTime: e.target.value }))} />
|
||||
</div>
|
||||
|
||||
{/* AI 汇总 */}
|
||||
@@ -1320,7 +1352,7 @@ function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: {
|
||||
<div className="text-xs text-muted-foreground">手动触发快照时生成 AI 四段式汇总</div>
|
||||
</div>
|
||||
<Switch checked={local.aiSummaryEnabled}
|
||||
onCheckedChange={(v) => setLocal(p => ({ ...p, aiSummaryEnabled: v }))} />
|
||||
onCheckedChange={(v) => edit(p => ({ ...p, aiSummaryEnabled: v }))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user