diff --git a/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index 6dbb9eaa..eeb700ad 100644 --- a/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -28,6 +28,8 @@ import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import type {DatabaseCleanupTarget} from "@/lib/services/openflare" import {NodeService, OptionService, StatusService, UpdateService, UptimeKumaService,} from "@/lib/services/openflare" +import {VersionUpgradeDialog} from "@/app/(main)/openflare/components/version-upgrade-dialog" +import {openflareLatestReleaseQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade" import { agentOptionEntries, @@ -82,6 +84,7 @@ export function OpenFlareOpsSettings() { label: string } | null>(null) const [cleanupRetentionDays, setCleanupRetentionDays] = useState("") + const [versionDialogOpen, setVersionDialogOpen] = useState(false) const optionsQuery = useQuery({ queryKey: optionsQueryKey, @@ -89,7 +92,7 @@ export function OpenFlareOpsSettings() { }) const statusQuery = useQuery({ - queryKey: ["openflare", "public-status"], + queryKey: openflarePublicStatusQueryKey, queryFn: () => StatusService.getPublicStatus(), }) @@ -99,7 +102,7 @@ export function OpenFlareOpsSettings() { }) const releaseQuery = useQuery({ - queryKey: ["openflare", "latest-release"], + queryKey: openflareLatestReleaseQueryKey("stable"), queryFn: () => UpdateService.getLatestRelease("stable"), }) @@ -602,11 +605,16 @@ export function OpenFlareOpsSettings() { - - 版本信息 - - 可通过 UpdateService 对接顶栏升级入口;完整升级流程可复用 Wavelet admin/updater。 - + +
+ 版本信息 + + 检查 GitHub Release、在线升级或手动上传服务端二进制包。 + +
+
@@ -635,6 +643,12 @@ export function OpenFlareOpsSettings() {
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default function AboutPage() { + const aboutQuery = useQuery({ + queryKey: aboutQueryKey, + queryFn: () => AboutService.getAboutContent(), + }) + + const statusQuery = useQuery({ + queryKey: publicStatusQueryKey, + queryFn: () => StatusService.getPublicStatus(), + }) + + const isLoading = aboutQuery.isLoading || statusQuery.isLoading + + if (isLoading) { + return + } + + if (aboutQuery.isError) { + return ( +
+ void aboutQuery.refetch()} + /> +
+ ) + } + + if (statusQuery.isError) { + return ( +
+ void statusQuery.refetch()} + /> +
+ ) + } + + const aboutContent = aboutQuery.data?.trim() ?? "" + const status = statusQuery.data + + if (!status) { + return ( +
+ +
+ ) + } + + return ( +
+
+ +
+

关于 OpenFlare

+

+ 公开展示当前系统简介、版本信息与项目入口。 +

+
+
+ + + +
+ 系统信息 + 当前运行实例的公开状态与项目链接。 +
+ {status.version || "dev"} +
+ +
+

+ 系统名称 +

+

+ {status.system_name || "OpenFlare"} +

+
+
+

+ Server 启动时间 +

+

+ {formatDateTime(new Date(status.start_time * 1000))} +

+
+
+

+ 项目仓库 +

+ + github.com/Rain-kl/OpenFlare + +
+
+
+ + {aboutContent ? ( + + + 项目介绍 + + 以下内容由系统设置中的「关于内容」维护。 + + + +
+ {aboutContent} +
+
+
+ ) : ( + + )} + +
+ + 返回总览 + +
+
+ ) +} \ No newline at end of file diff --git a/Wavelet/frontend/app/(main)/openflare/access-logs/page.tsx b/Wavelet/frontend/app/(main)/openflare/access-logs/page.tsx index 81974df5..0b544c1f 100644 --- a/Wavelet/frontend/app/(main)/openflare/access-logs/page.tsx +++ b/Wavelet/frontend/app/(main)/openflare/access-logs/page.tsx @@ -394,7 +394,18 @@ export default function AccessLogsPage() { - {foldsQuery.isLoading ? ( + {foldsQuery.isError ? ( +
+ void foldsQuery.refetch()} + /> +
+ ) : foldsQuery.isLoading ? ( ) : (foldsQuery.data?.items ?? []).length === 0 ? ( @@ -455,7 +466,18 @@ export default function AccessLogsPage() { - {ipSummaryQuery.isLoading ? ( + {ipSummaryQuery.isError ? ( +
+ void ipSummaryQuery.refetch()} + /> +
+ ) : ipSummaryQuery.isLoading ? ( ) : (ipSummaryQuery.data?.items ?? []).length === 0 ? ( diff --git a/Wavelet/frontend/app/(main)/openflare/components/upgrade-log-panel.tsx b/Wavelet/frontend/app/(main)/openflare/components/upgrade-log-panel.tsx new file mode 100644 index 00000000..eb3ce96b --- /dev/null +++ b/Wavelet/frontend/app/(main)/openflare/components/upgrade-log-panel.tsx @@ -0,0 +1,71 @@ +'use client'; + +import {Badge} from '@/components/ui/badge'; +import type {LatestReleaseInfo} from '@/lib/services/openflare'; + +function formatLogTimestamp(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return '0000-00-00 00:00:00.000'; + } + + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + const hour = `${date.getHours()}`.padStart(2, '0'); + const minute = `${date.getMinutes()}`.padStart(2, '0'); + const second = `${date.getSeconds()}`.padStart(2, '0'); + const millisecond = `${date.getMilliseconds()}`.padStart(3, '0'); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}.${millisecond}`; +} + +function formatLogLevel(level: string) { + return (level || 'info').toUpperCase().padEnd(8, ' '); +} + +function getUpgradeStatusBadge(release: LatestReleaseInfo) { + if (release.upgrade_status === 'failed') { + return { label: '升级失败', variant: 'destructive' as const }; + } + if (release.upgrade_status === 'succeeded') { + return { label: '准备重启', variant: 'default' as const }; + } + if (release.in_progress) { + return { label: '升级中', variant: 'secondary' as const }; + } + return { label: '空闲', variant: 'outline' as const }; +} + +export function UpgradeLogPanel({release}: {release: LatestReleaseInfo}) { + const statusBadge = getUpgradeStatusBadge(release); + const upgradeLogs = release.upgrade_logs ?? []; + + if (upgradeLogs.length === 0) { + return ( +
+ 暂无升级日志 +
+ ); + } + + return ( +
+ {statusBadge.label} +
+ {upgradeLogs.map((log, index) => ( +
+            {formatLogTimestamp(log.created_at)}
+             | 
+            {formatLogLevel(log.level)}
+             | 
+            {log.message}
+          
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/Wavelet/frontend/app/(main)/openflare/components/version-upgrade-dialog.tsx b/Wavelet/frontend/app/(main)/openflare/components/version-upgrade-dialog.tsx new file mode 100644 index 00000000..610e1a04 --- /dev/null +++ b/Wavelet/frontend/app/(main)/openflare/components/version-upgrade-dialog.tsx @@ -0,0 +1,401 @@ +'use client'; + +import {useEffect, useState} from 'react'; +import {ExternalLink, Loader2} from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; +import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,} from '@/components/ui/dialog'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; +import {Progress} from '@/components/ui/progress'; +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; +import {useOpenFlareServerUpgrade} from '@/lib/hooks/use-openflare-server-upgrade'; +import type {LatestReleaseInfo, ReleaseChannel} from '@/lib/services/openflare'; +import {formatDateTime} from '@/lib/utils'; +import {formatRelativeTime} from '@/app/(main)/openflare/nodes/components/node-utils'; + +import {UpgradeLogPanel} from './upgrade-log-panel'; + +function getUpgradeBadge(release: LatestReleaseInfo | null | undefined) { + if (!release) { + return { label: '未检查', variant: 'outline' as const }; + } + if (release.in_progress) { + return { label: '升级中', variant: 'secondary' as const }; + } + if (release.has_update) { + return { label: '可升级', variant: 'secondary' as const }; + } + return { label: '最新', variant: 'default' as const }; +} + +export function VersionUpgradeDialog({ + open, + onOpenChange, + canUpgrade = true, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + canUpgrade?: boolean; +}) { + const { + currentVersion, + selectedChannel, + release, + uploadedBinary, + releaseErrorMessage, + manualStatusMessage, + manualErrorMessage, + isInitialLoading, + isChecking, + isUpgrading, + isUploadingBinary, + uploadProgress, + isConfirmingManualUpgrade, + handleOpen, + handleCheckRelease, + handleChannelChange, + handleUpgrade, + handleUploadBinary, + handleConfirmManualUpgrade, + } = useOpenFlareServerUpgrade({ open, canUpgrade }); + const [selectedBinary, setSelectedBinary] = useState(null); + + useEffect(() => { + if (open) { + handleOpen(); + } + }, [open, handleOpen]); + + useEffect(() => { + if (!open) { + setSelectedBinary(null); + } + }, [open]); + + const upgradeBadge = getUpgradeBadge(release); + const selectedChannelLabel = selectedChannel === 'preview' ? '预览版' : '正式版'; + const uploadPhaseLabel = + uploadProgress >= 100 ? '已上传,正在服务端校验版本...' : '上传中...'; + const canConfirmManualUpgrade = Boolean( + uploadedBinary?.ready_to_upgrade && uploadedBinary.upload_token, + ); + + const isBusy = + isChecking || isUpgrading || isUploadingBinary || isConfirmingManualUpgrade; + + return ( + + + + 服务端版本 + + 默认检查正式版更新;也可检查 preview 发布或手动上传二进制包。升级开始后服务会短暂重启。 + + + +
+
+ + + 当前版本 + + +
+

{currentVersion}

+ {upgradeBadge.label} +
+
+
+ + + + 最新版本 + + +
+

{release?.tag_name || '未检查'}

+ {canUpgrade ? ( + + ) : null} +
+ {canUpgrade ? ( + + ) : null} +
+
+
+ + {isInitialLoading ? ( +
+ + 加载版本信息... +
+ ) : null} + + {!isInitialLoading && releaseErrorMessage ? ( +
+ {releaseErrorMessage} +
+ ) : null} + + + + 在线升级 + {canUpgrade ? 手动升级 : null} + {release ? 升级日志 : null} + + + + {!isInitialLoading && !releaseErrorMessage && !release ? ( +
+ 尚未检查{selectedChannelLabel},点击「检查{selectedChannelLabel}」后展示 GitHub Release 信息。 +
+ ) : null} + + {release ? ( + + + + GitHub {selectedChannelLabel} · {release.tag_name} + + + {release.published_at + ? `发布时间:${formatRelativeTime(release.published_at)} · ${formatDateTime(release.published_at)}` + : '未提供发布时间'} + + + +
+ + {release.has_update ? '发现新版本' : '已经是最新版本'} + + {release.prerelease ? ( + Preview 发布 + ) : ( + 正式发布 + )} + {!release.upgrade_supported ? ( + 当前平台不支持自动升级 + ) : null} + {release.in_progress ? ( + 升级任务执行中 + ) : null} +
+ +
+ + {release.body || '暂无更新说明'} + +
+ + {release.html_url ? ( + + 查看发布详情 + + + ) : null} + + {canUpgrade ? ( +
+ +
+ ) : null} +
+
+ ) : null} +
+ + {canUpgrade ? ( + + + + 手动升级 + + 支持上传已编译好的当前平台 Server 可执行文件。 + + + +
+ + { + setSelectedBinary(event.target.files?.[0] ?? null); + }} + /> +
+ +
+ {canConfirmManualUpgrade ? ( + + ) : ( + + )} +
+ + {isUploadingBinary ? ( +
+
+ {uploadPhaseLabel} + {uploadProgress}% +
+ +
+ ) : null} + + {manualErrorMessage ? ( +
+ {manualErrorMessage} +
+ ) : null} + + {!manualErrorMessage && manualStatusMessage ? ( +
+ {manualStatusMessage} +
+ ) : null} + + {uploadedBinary ? ( +
+
+ + {uploadedBinary.ready_to_upgrade + ? '可确认升级' + : uploadedBinary.has_update + ? '待确认' + : '不可升级'} + + {!uploadedBinary.upgrade_supported ? ( + 当前版本不支持手动升级 + ) : null} +
+ +
+ + + + +
+ +

+ {uploadedBinary.comparison_message} +

+
+ ) : ( +
+ 尚未上传升级包 +
+ )} +
+
+
+ ) : null} + + {release ? ( + + + + ) : null} +
+
+
+
+ ); +} + +function InfoCell({label, value}: {label: string; value: string}) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +export type {ReleaseChannel}; diff --git a/Wavelet/frontend/app/(main)/openflare/config-versions/components/cleanup-dialog.tsx b/Wavelet/frontend/app/(main)/openflare/config-versions/components/cleanup-dialog.tsx index ccaef7a0..12dbfd17 100644 --- a/Wavelet/frontend/app/(main)/openflare/config-versions/components/cleanup-dialog.tsx +++ b/Wavelet/frontend/app/(main)/openflare/config-versions/components/cleanup-dialog.tsx @@ -1,7 +1,10 @@ "use client" -import {useState} from "react" +import {useEffect} from "react" +import {zodResolver} from "@hookform/resolvers/zod" import {Loader2} from "lucide-react" +import {useForm} from "react-hook-form" +import {z} from "zod" import { AlertDialog, @@ -16,6 +19,12 @@ import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" import {Label} from "@/components/ui/label" +const cleanupSchema = z.object({ + keepCount: z.number().int().min(3, "最少保留 3 个历史快照"), +}) + +type CleanupFormValues = z.infer + interface CleanupDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -29,17 +38,20 @@ export function CleanupDialog({ onConfirm, loading, }: CleanupDialogProps) { - const [keepCount, setKeepCount] = useState(10) - const [error, setError] = useState(null) + const form = useForm({ + resolver: zodResolver(cleanupSchema), + defaultValues: { keepCount: 10 }, + }) - const handleConfirm = () => { - if (keepCount < 3) { - setError("最少保留 3 个历史快照") - return + useEffect(() => { + if (open) { + form.reset({ keepCount: 10 }) } - setError(null) - onConfirm(keepCount) - } + }, [form, open]) + + const handleConfirm = form.handleSubmit((values) => { + onConfirm(values.keepCount) + }) return ( @@ -57,17 +69,18 @@ export function CleanupDialog({ id="keepCount" type="number" min={3} - value={keepCount} - onChange={(e) => setKeepCount(Number.parseInt(e.target.value, 10) || 3)} disabled={loading} + {...form.register("keepCount", { valueAsNumber: true })} />

默认为 10 个,最少需保留 3 个。

- {error ?

{error}

: null} + {form.formState.errors.keepCount ? ( +

{form.formState.errors.keepCount.message}

+ ) : null} 取消 -