From a2d17be0c4635d7bb3bb1d1c019367d1e4fa3b2e Mon Sep 17 00:00:00 2001 From: cneicy Date: Sat, 26 Sep 2026 13:35:14 +0800 Subject: [PATCH] feat: share status layout and prioritize operational groups --- CUSTOMIZATION.md | 6 +- Dockerfile.compact-uptime | 4 +- Dockerfile.local-release | 2 +- VERSION | 2 +- .../group-status/__tests__/metrics.test.tsx | 70 ++++++++++++++++++- .../__tests__/status-page.test.tsx | 13 ++-- web/src/features/group-status/index.tsx | 24 ++++--- web/src/features/group-status/lib/status.ts | 8 +++ web/src/features/group-status/status-page.tsx | 37 ++++++---- 9 files changed, 131 insertions(+), 35 deletions(-) diff --git a/CUSTOMIZATION.md b/CUSTOMIZATION.md index 261bd45..a385098 100644 --- a/CUSTOMIZATION.md +++ b/CUSTOMIZATION.md @@ -1,6 +1,6 @@ # 自定义版本说明 -当前版本为 v1.0.0-rc.38-custom.42-20260926,基于官方 v1.0.0-rc.38,对应 2026-09-26 已部署的自定义源码。 +当前版本为 v1.0.0-rc.38-custom.43-20260926,基于官方 v1.0.0-rc.38,对应 2026-09-26 已部署的自定义源码。 ## 主要定制 @@ -11,13 +11,13 @@ - 独立公开 uptime 页,使用 shadcn/ui,支持分组别名、隐藏分组和管理员重置当前分组检测记录。 - 主站 /group-status 使用真实分组名并展示全部分组,检测请求使用真实分组标识;独立 uptime 页应用公开展示配置。 - 渠道每 15 分钟探活,展示最近 18 次记录;正常用户请求可恢复分组正常状态。 -- 支持 Astra 的分组排在前面,其他分组使用紧凑布局;鹈鹕样例按时间倒序横向滚动,最新在左,点击缩略图预览。 +- 两页共用分组行布局,正常状态优先,同类状态中支持 Astra 的分组排在前面,其他分组使用紧凑布局;鹈鹕样例按时间倒序横向滚动,最新在左,点击缩略图预览。 - 鹈鹕检测通过 manxue.ai 访客接口请求公开作品,保存可跳转的作品链接;连接失败时本地生成并标记为不确定。 - 作品脚本在隔离 iframe 中运行,以保留腿部、踏板等动画绘制;缩略图按容器宽度等比缩放。 ## 从源码构建 - docker build -f Dockerfile.local-release -t local/new-api:v1.0.0-rc.38-custom.42-20260926 . + docker build -f Dockerfile.local-release -t local/new-api:v1.0.0-rc.38-custom.43-20260926 . 此入口从 Bun、Go 和 Debian 基础镜像构建前端与服务,不要求预先存在旧版 local/new-api 镜像。VERSION 用于编译版本号;Compose 如果设置了 VERSION 环境变量,也应同步更新。 diff --git a/Dockerfile.compact-uptime b/Dockerfile.compact-uptime index f313c94..78b328e 100644 --- a/Dockerfile.compact-uptime +++ b/Dockerfile.compact-uptime @@ -1,3 +1,3 @@ -FROM local/new-api:v1.0.0-rc.38-custom.41-20260926 +FROM local/new-api:v1.0.0-rc.38-custom.42-20260926 COPY new-api /new-api -LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.42-20260926" +LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.43-20260926" diff --git a/Dockerfile.local-release b/Dockerfile.local-release index bd4d5ca..e1d55e2 100644 --- a/Dockerfile.local-release +++ b/Dockerfile.local-release @@ -25,7 +25,7 @@ RUN apt-get update \ COPY --from=build /build/new-api / COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/ COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/ -LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.42-20260926" +LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.43-20260926" EXPOSE 3000 WORKDIR /data ENTRYPOINT ["/new-api"] diff --git a/VERSION b/VERSION index b6ad8f4..9e7131b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.0-rc.38-custom.42-20260926 \ No newline at end of file +v1.0.0-rc.38-custom.43-20260926 \ No newline at end of file diff --git a/web/src/features/group-status/__tests__/metrics.test.tsx b/web/src/features/group-status/__tests__/metrics.test.tsx index 433085e..7a4e82d 100644 --- a/web/src/features/group-status/__tests__/metrics.test.tsx +++ b/web/src/features/group-status/__tests__/metrics.test.tsx @@ -13,9 +13,75 @@ import { api } from '@/lib/api' import { useAuthStore } from '@/stores/auth-store' import { FingerprintRetest } from '../fingerprint-panel' -import { MonitorCard } from '../index' +import { GroupStatus, MonitorCard } from '../index' -vi.mock('@/components/layout', () => ({ PublicLayout: () => null })) +vi.mock('@/components/layout', () => ({ + PublicLayout: ({ children }: { children: React.ReactNode }) => children, +})) + +it('uses the shared rows while preserving main-site names, artworks and metrics', async () => { + const get = vi.spyOn(api, 'get').mockResolvedValue({ + data: { + success: true, + data: [ + { + categoryName: 'Groups', + monitors: [ + { + name: '#alias', + group: 'real-astra', + status: 1, + uptime: 1, + models: ['gpt-6-astra'], + metrics: { + avg_ttft_ms: 10730, + avg_latency_ms: 22250, + avg_tps: 26.6, + success_rate: 100, + }, + pelican: { + channels: 1, + normal: 1, + degraded: 0, + unknown: 0, + failed: 0, + running: 0, + history: [ + { + id: 'hidden-uptime-work', + at: 1, + status: 'succeeded', + quality: 'normal', + user_initiated: false, + has_html: true, + }, + ], + }, + }, + ], + }, + ], + }, + }) + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) + render( + + + + ) + await screen.findByRole('heading', { name: 'real-astra' }) + expect(get).toHaveBeenCalledWith('/api/group-status') + expect( + screen.queryByRole('heading', { name: '#alias' }) + ).not.toBeInTheDocument() + expect( + screen.getByTitle(t('Pelican sample {{number}}', { number: 1 })) + ).toHaveAttribute('src', '/api/group-status/artwork/hidden-uptime-work') + fireEvent.click(screen.getByRole('button', { name: t('Detection details') })) + expect(await screen.findByText('10.7 s')).toBeInTheDocument() +}) afterEach(() => { cleanup() diff --git a/web/src/features/group-status/__tests__/status-page.test.tsx b/web/src/features/group-status/__tests__/status-page.test.tsx index fd4c6dc..3a42946 100644 --- a/web/src/features/group-status/__tests__/status-page.test.tsx +++ b/web/src/features/group-status/__tests__/status-page.test.tsx @@ -194,8 +194,9 @@ it('keeps non-Astra groups compact and puts Astra groups first', async () => { data: [ { categoryName: 'Groups', - monitors: [ - { name: 'Luna only', status: 1, uptime: 1, models: ['gpt-6-luna'] }, + monitors: [ + { name: 'Failed Astra', status: 0, uptime: 0, models: ['gpt-6-astra'] }, + { name: 'Luna only', status: 1, uptime: 1, models: ['gpt-6-luna'] }, { name: 'Astra group', status: 1, @@ -213,12 +214,12 @@ it('keeps non-Astra groups compact and puts Astra groups first', async () => { screen .getAllByRole('heading', { level: 2 }) .map((heading) => heading.textContent) - ).toEqual(['Astra group', 'Luna only']) - expect(screen.getAllByText(t('Recent pelican works'))).toHaveLength(1) - expect(screen.getAllByText(t('Model fingerprint'))).toHaveLength(1) + ).toEqual(['Astra group', 'Luna only', 'Failed Astra']) + expect(screen.getAllByText(t('Recent pelican works'))).toHaveLength(2) + expect(screen.getAllByText(t('Model fingerprint'))).toHaveLength(2) expect( screen.getAllByRole('region', { name: t('Availability history') }) - ).toHaveLength(2) + ).toHaveLength(3) }) it('keeps the main-site group status link on the original internal route', () => { diff --git a/web/src/features/group-status/index.tsx b/web/src/features/group-status/index.tsx index a47f0bc..20ad4a3 100644 --- a/web/src/features/group-status/index.tsx +++ b/web/src/features/group-status/index.tsx @@ -27,12 +27,14 @@ import { DetectionActions, PelicanPanel } from './detection-panel' import { FingerprintPanel } from './fingerprint-panel' import { cacheHitRates, + compareStatusMonitors, formatCacheHitRate, getOverallStatus, statusKind, type GroupStatusKind, } from './lib/status' import { ProbeHistory } from './probe-history' +import { StatusGroupRow } from './status-page' import { ResetGroupDetections } from './status-page-settings' type StatusKind = GroupStatusKind @@ -191,9 +193,9 @@ export function GroupStatus() { }) const monitors = useMemo( () => - (query.data?.data || []).flatMap( - (group: UptimeGroupResult) => group.monitors || [] - ), + (query.data?.data || []) + .flatMap((group: UptimeGroupResult) => group.monitors || []) + .sort(compareStatusMonitors), [query.data?.data] ) const overall = getOverallStatus(monitors.map((monitor) => monitor.status)) @@ -205,7 +207,7 @@ export function GroupStatus() { ) if (query.isLoading) { content = ( -
+
{[0, 1, 2].map((item) => ( ))} @@ -219,13 +221,19 @@ export function GroupStatus() { ) } else if (monitors.length) { content = ( -
+
{monitors.map((monitor) => ( - + } + detailsContent={} /> ))}
@@ -234,7 +242,7 @@ export function GroupStatus() { return ( -
+

{t('Group Status')}

diff --git a/web/src/features/group-status/lib/status.ts b/web/src/features/group-status/lib/status.ts index b78e010..4c753e5 100644 --- a/web/src/features/group-status/lib/status.ts +++ b/web/src/features/group-status/lib/status.ts @@ -2,6 +2,14 @@ import type { UptimeMonitor, UptimeSample } from '@/features/dashboard/types' export type GroupStatusKind = 'operational' | 'degraded' | 'error' | 'unknown' +export function supportsAstra(monitor: UptimeMonitor): boolean { + return monitor.models?.includes('gpt-6-astra') ?? false +} + +export function compareStatusMonitors(a: UptimeMonitor, b: UptimeMonitor): number { + return Number(b.status === 1) - Number(a.status === 1) || Number(supportsAstra(b)) - Number(supportsAstra(a)) +} + export function statusKind(status: number): GroupStatusKind { if (status < 0) return 'unknown' if (status === 1) return 'operational' diff --git a/web/src/features/group-status/status-page.tsx b/web/src/features/group-status/status-page.tsx index a687355..c34d681 100644 --- a/web/src/features/group-status/status-page.tsx +++ b/web/src/features/group-status/status-page.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { Activity, ArrowUpRight, RefreshCw, Settings2 } from 'lucide-react' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { Dialog } from '@/components/dialog' @@ -19,7 +19,12 @@ import { cn } from '@/lib/utils' import { useAuthStore } from '@/stores/auth-store' import { FingerprintPanel } from './fingerprint-panel' -import { getOverallStatus, statusKind } from './lib/status' +import { + getOverallStatus, + statusKind, + supportsAstra, + compareStatusMonitors, +} from './lib/status' import { ProbeHistory } from './probe-history' import { getStatusPage } from './status-page-api' import { @@ -41,11 +46,11 @@ function qualityLabel(quality: string): string { return 'Unable to classify' } -function supportsAstra(monitor: UptimeMonitor): boolean { - return monitor.models?.includes('gpt-6-astra') ?? false -} - -function ArtworkThumbnail(props: { id: string; title: string }) { +function ArtworkThumbnail(props: { + id: string + title: string + artworkBase: string +}) { const container = useRef(null) const [scale, setScale] = useState(0) useEffect(() => { @@ -64,7 +69,7 @@ function ArtworkThumbnail(props: { id: string; title: string }) { >