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

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useCallback, lazy, Suspense, useState, useEffect } from "react";
import { AlertTriangle, Bell, Activity, Network, FileText } from "lucide-react";
import { AlertTriangle, Bell, Activity, Network, FileText, BarChart3 } from "lucide-react";
import { WorkbenchView } from "./WorkbenchView";
import { deleteVaultKey } from "../../application/defaultKeyPassphrases";
import { usePluginImporterCommit } from "../../application/state/usePluginImporterCommit";
import { preserveConcurrentHostLineTimestampUpdate } from "../../domain/host";
@@ -22,6 +23,7 @@ import {
} from "./VaultPageHeader";
import { useConnectionLogsStore } from "../../application/state/connectionLogsStore";
import { useNotesStore } from "../../application/state/notesStore";
import { useSettingsState } from "../../application/state/useSettingsState";
import { LazyLoadBoundary } from "../ui/lazy-load-boundary";
import { toast } from "../ui/toast";
import { AppWordmark } from "../AppWordmark";
@@ -35,6 +37,8 @@ const InspectionCenterPage = lazy(() => import("../inspection/InspectionCenterPa
const MonitoringTopologyView = lazy(() => import("../monitoring/MonitoringTopologyView"));
// 巡检报告
const ReportPage = lazy(() => import("../inspection/ReportPage"));
// 设置页面(远程访问降级用)
const SettingsPage = lazy(() => import("../SettingsPage"));
type VaultViewLayoutContext = Record<string, any>;
@@ -111,10 +115,12 @@ function VaultConnectionLogsSection({
ConnectionLogsManager,
hosts,
onOpenLogView,
initialLogId,
}: {
ConnectionLogsManager: React.ComponentType<any>;
hosts: any[];
onOpenLogView: (log: any) => void;
initialLogId?: string;
}) {
const {
connectionLogs,
@@ -122,6 +128,14 @@ function VaultConnectionLogsSection({
deleteConnectionLog,
clearUnsavedConnectionLogs,
} = useConnectionLogsStore();
// 从工作台跳转时自动打开指定 log 的详情
React.useEffect(() => {
if (!initialLogId) return;
const log = connectionLogs.find((l: any) => l.id === initialLogId);
if (log) onOpenLogView(log);
}, [initialLogId, connectionLogs, onOpenLogView]);
return (
<ConnectionLogsManager
logs={connectionLogs}
@@ -137,6 +151,34 @@ function VaultConnectionLogsSection({
export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
// ─── 设置权威源同步(远程浏览器) ───
// 1. HTML 注入脚本里的同步 XHR 已在 React 启动前把权威设置填到 localStorage
// 2. 这里注册写后钩子:每次 localStorageAdapter 写都 POST 回权威源
React.useEffect(() => {
if (!IS_REMOTE) return;
const TOKEN = (window as any).__REMOTE_TOKEN__;
const API_BASE = `${window.location.origin}`;
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = (key: string, value: string | null) => {
fetch(`${API_BASE}/api/settings?token=${encodeURIComponent(TOKEN)}`, {
method: value === null ? "DELETE" : "POST",
headers: { "Content-Type": "application/json" },
body: value === null ? JSON.stringify({ keys: [key] }) : JSON.stringify({ [key]: value }),
}).catch(() => {});
};
return () => {
if ((globalThis as any).__NETCATTY_SETTINGS_SYNC__) {
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = null;
}
};
}, [IS_REMOTE]);
const remoteSettings = useSettingsState();
// 工作台跳转时带过来的预选中项(alertId / logId / hostId)
const pendingSelectionRef = React.useRef<{ alertId?: string; logId?: string; hostId?: string } | null>(null);
// 产品名称 - 直接读取 localStorage,监听 storage 事件(来自其他标签页)和自定义事件
const [productName, setProductName] = useState(() => localStorageAdapter.readString(STORAGE_KEY_PRODUCT_NAME) || 'NetMesh');
@@ -444,6 +486,16 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
),
[displayedGroups, viewMode, visibleTreeGroupPaths],
);
// 当切到 hosts section 且带了 hostId,自动选中
React.useEffect(() => {
if (currentSection !== "hosts") return;
const pending = pendingSelectionRef.current;
if (pending?.hostId) {
setSelectedHostIds(new Set([pending.hostId]));
pendingSelectionRef.current = null; // 消费掉
}
}, [currentSection, setSelectedHostIds]);
React.useEffect(() => {
setSelectedGroupPaths((current: Set<string>) => {
const next = retainVisibleVaultGroupSelection(current, visibleSelectableGroupPaths);
@@ -457,6 +509,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
resizeAriaLabel: t("vault.panel.resizeWidth"),
};
const [isSidebarResizing, setIsSidebarResizing] = React.useState(false);
const [showRemoteSettings, setShowRemoteSettings] = React.useState(false);
const keyListRef = React.useRef(keys);
keyListRef.current = keys;
const newHostActionsRef = React.useRef<HTMLDivElement>(null);
@@ -600,6 +653,30 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
<div
className={cn("space-y-1", sidebarCollapsed ? "px-1.5" : "px-2.5")}
>
{/* 工作台 */}
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
variant={currentSection === "workbench" ? "secondary" : "ghost"}
className={cn(
"w-full h-10",
sidebarCollapsed
? "justify-center p-0"
: "justify-start gap-3",
currentSection === "workbench" &&
"bg-foreground/10 text-foreground hover:bg-foreground/15 border-border/40",
)}
onClick={() => setCurrentSection("workbench")}
>
<BarChart3 size={16} className="flex-shrink-0" />
{!sidebarCollapsed && "工作台"}
</RippleButton>
</TooltipTrigger>
{sidebarCollapsed && (
<TooltipContent side="right">工作台</TooltipContent>
)}
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
@@ -948,7 +1025,13 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
? "justify-center p-0"
: "justify-start gap-3",
)}
onClick={onOpenSettings}
onClick={() => {
if (IS_REMOTE) {
setShowRemoteSettings(true);
} else {
onOpenSettings();
}
}}
>
<Settings size={16} className="flex-shrink-0" />
{!sidebarCollapsed && t("common.settings")}
@@ -1383,6 +1466,27 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
}}
/>
{/* 工作台 */}
{currentSection === "workbench" && (
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<WorkbenchView
hosts={hosts}
keys={keys}
proxies={[]}
snippetCount={snippets?.length ?? 0}
activePortForwardCount={0}
onNavigate={(section: string, opts?: any) => {
if (opts) pendingSelectionRef.current = opts;
setCurrentSection(section);
}}
onConnectHost={(hostId: string) => {
const h = hosts?.find((x: any) => x.id === hostId);
if (h) handleHostConnect(h);
}}
/>
</div>
)}
{currentSection === "snippets" && (
<LazyLoadBoundary name="Snippets" resetKey="snippets">
<Suspense fallback={<VaultSectionLoading />}>
@@ -1556,6 +1660,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
ConnectionLogsManager={LazyConnectionLogsManager}
hosts={hosts}
onOpenLogView={onOpenLogView}
initialLogId={pendingSelectionRef.current?.logId ?? undefined}
/>
</Suspense>
</LazyLoadBoundary>
@@ -1580,7 +1685,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
<Suspense fallback={<VaultSectionLoading />}>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<InspectionErrorBoundary>
<AlertCenterPage />
<AlertCenterPage initialAlertId={pendingSelectionRef.current?.alertId ?? undefined} />
</InspectionErrorBoundary>
</div>
</Suspense>
@@ -2028,6 +2133,25 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
onUpdateHosts([...hosts, host]);
}}
/>
{/* Remote Settings Dialog (远程访问设置面板) */}
{IS_REMOTE && (
<Dialog open={showRemoteSettings} onOpenChange={setShowRemoteSettings}>
<DialogContent className="max-w-5xl max-h-[92vh] overflow-auto p-0 gap-0">
<DialogHeader className="px-6 pt-5 pb-3 border-b border-border">
<DialogTitle>{t("common.settings")}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
<Suspense fallback={<div className="p-8 text-center text-muted-foreground">加载设置中...</div>}>
<SettingsPage
settings={remoteSettings as any}
appLock={{ enabled: false, locked: false } as any}
/>
</Suspense>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
}