'use client'; import Link from 'next/link'; import {useRouter} from 'next/navigation'; import {useMutation, useQueryClient} from '@tanstack/react-query'; import {useState} from 'react'; import {ArrowLeft, FileText, RefreshCw, RotateCcw, Trash2, Upload,} from 'lucide-react'; import {toast} from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; import {formatDateTime} from '@/lib/utils'; import type {NodeAgentReleaseInfo, NodeItem, ReleaseChannel} from '@/lib/services/openflare'; import {NodeService} from '@/lib/services/openflare'; import {AgentUpdateDialog} from './agent-update-dialog'; import {InstallCommand} from './install-command'; import {NodeEditorDialog} from './node-editor-dialog'; import {NodeObservability} from './node-observability'; import {NodeStatusBadge} from './node-status-badge'; import { formatRelativeTime, getApplyLabel, getApplyTone, getErrorMessage, getFlaredStatusLabel, getFlaredStatusTone, getNodeStatusLabel, getNodeStatusTone, isMeaningfulTime, isWSConnectedLastSeen, } from './node-utils'; const nodesQueryKey = ['openflare', 'nodes']; export function TunnelNodeDetail({ node }: { node: NodeItem }) { const router = useRouter(); const queryClient = useQueryClient(); const [editorOpen, setEditorOpen] = useState(false); const [upgradeOpen, setUpgradeOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const saveMutation = useMutation({ mutationFn: (payload: Parameters[1]) => NodeService.updateNode(node.id, payload), onSuccess: async () => { toast.success('隧道节点已更新'); setEditorOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const forceSyncMutation = useMutation({ mutationFn: () => NodeService.requestForceSync(node.id), onSuccess: async (updated) => { toast.success(`已向隧道节点 ${updated.name} 下发强制同步指令`); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const upgradeMutation = useMutation({ mutationFn: ({ release, channel, }: { release: NodeAgentReleaseInfo | null; channel: ReleaseChannel; }) => NodeService.requestAgentUpdate(node.id, { channel: release?.channel ?? channel, tag_name: release?.channel === 'preview' ? release.tag_name || undefined : undefined, }), onSuccess: async (updated) => { toast.success( `已向隧道节点 ${updated.name} 下发${updated.update_channel === 'preview' ? '预览版' : '正式版'}升级指令`, ); setUpgradeOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const deleteMutation = useMutation({ mutationFn: () => NodeService.deleteNode(node.id), onSuccess: async () => { toast.success('隧道节点已删除'); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); router.push('/nodes'); }, onError: (error) => toast.error(getErrorMessage(error)), }); const handleRefresh = () => { void Promise.all([ queryClient.invalidateQueries({ queryKey: nodesQueryKey }), queryClient.invalidateQueries({ queryKey: ['openflare', 'node-observability', node.id] }), ]); }; return (

{node.name}

节点 ID {node.node_id} openflared 版本 {node.version || 'unknown'} 当前配置版本 {node.current_version || '未应用'} 最近心跳 {isWSConnectedLastSeen(node.last_seen_at) ? 'WS 已连接' : isMeaningfulTime(node.last_seen_at) ? formatRelativeTime(node.last_seen_at) : '暂无'}
隧道客户端 隧道客户端节点 (tunnel_client / openflared)
运行状态
flared 状态
Tunnel Token {node.access_token || '暂无'}
IP 地址 {node.ip || '—'} {node.ip_manual_override ? '(已锁定)' : ''}
最近应用
自动更新 {node.auto_update_enabled ? '已启用' : '手动'}
创建时间 {formatDateTime(node.created_at)}
更新时间 {formatDateTime(node.updated_at)}
{node.last_error ? (
{node.last_error}
) : null}
同步状态 配置追平与应用结果
最近应用结果
最近应用时间 {isMeaningfulTime(node.latest_apply_at) && node.latest_apply_at ? `${formatRelativeTime(node.latest_apply_at)} · ${formatDateTime(node.latest_apply_at)}` : '暂无'}
{node.latest_apply_checksum ? (
同步文件数 {node.latest_support_file_count} 个
) : null}
升级状态
最近心跳详情 {isWSConnectedLastSeen(node.last_seen_at) ? 'WebSocket 长连接已建立' : isMeaningfulTime(node.last_seen_at) ? formatDateTime(node.last_seen_at) : '暂无'}
setEditorOpen(false)} onSubmit={async (payload) => { await saveMutation.mutateAsync(payload); }} /> setUpgradeOpen(false)} onConfirm={async (release, channel) => { await upgradeMutation.mutateAsync({ release, channel }); }} /> 确认删除隧道节点 确认删除隧道节点「{node.name}」吗?删除后该节点需要重新创建并重新接入。 取消 deleteMutation.mutate()} > {deleteMutation.isPending ? '删除中...' : '确认删除'}
); }