feat: share status layout and prioritize operational groups

This commit is contained in:
cneicy committed 2026-09-26 13:35:14 +08:00
1 parent ceddfd3d6b
commit da8947e225
9 files changed
+131 -35

No files matched your search

+3 -3
View File
@@ -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 环境变量,也应同步更新。
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -1 +1 @@
v1.0.0-rc.38-custom.42-20260926
v1.0.0-rc.38-custom.43-20260926
@@ -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(
<QueryClientProvider client={client}>
<GroupStatus />
</QueryClientProvider>
)
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()
@@ -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', () => {
+16 -8
View File
@@ -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<UptimeMonitor[]>(
() =>
(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 = (
<div className='mt-7 grid gap-5 md:grid-cols-2 xl:grid-cols-3'>
<div className='mt-7 space-y-5'>
{[0, 1, 2].map((item) => (
<Skeleton key={item} className='h-[400px] rounded-lg' />
))}
@@ -219,13 +221,19 @@ export function GroupStatus() {
)
} else if (monitors.length) {
content = (
<div className='mt-7 grid gap-5 md:grid-cols-2 xl:grid-cols-3'>
<div className='mt-7 space-y-5'>
{monitors.map((monitor) => (
<MonitorCard
<StatusGroupRow
key={`${monitor.group}-${monitor.name}`}
monitor={monitor}
monitor={{ ...monitor, name: monitor.group || monitor.name }}
updatedAt={query.dataUpdatedAt}
refreshing={query.isFetching}
artworkBase='/api/group-status/artwork/'
actions={
!!monitor.fingerprint?.channels &&
!!monitor.group && <DetectionActions group={monitor.group} />
}
detailsContent={<MonitorCard monitor={monitor} />}
/>
))}
</div>
@@ -234,7 +242,7 @@ export function GroupStatus() {
return (
<PublicLayout showMainContainer={false}>
<main className='mx-auto w-full max-w-7xl min-w-0 px-4 pt-24 pb-12 md:px-6'>
<main className='mx-auto w-full max-w-[1680px] min-w-0 px-5 pt-24 pb-12 md:px-8'>
<div className='flex flex-wrap items-end justify-between gap-4'>
<div>
<h1 className='text-3xl font-bold'>{t('Group Status')}</h1>
@@ -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'
+25 -12
View File
@@ -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<HTMLDivElement>(null)
const [scale, setScale] = useState(0)
useEffect(() => {
@@ -64,7 +69,7 @@ function ArtworkThumbnail(props: { id: string; title: string }) {
>
<iframe
title={props.title}
src={'/api/detections/artwork/' + props.id}
src={props.artworkBase + props.id}
sandbox='allow-scripts'
referrerPolicy='no-referrer'
loading='lazy'
@@ -102,10 +107,14 @@ export function StatusGroupRow(props: {
monitor: UptimeMonitor
updatedAt?: number
refreshing?: boolean
artworkBase?: string
actions?: ReactNode
detailsContent?: ReactNode
}) {
const { t, i18n } = useTranslation()
const monitor = props.monitor
const astra = supportsAstra(monitor)
const artworkBase = props.artworkBase || '/api/detections/artwork/'
const [selected, setSelected] = useState('')
const [details, setDetails] = useState(false)
const user = useAuthStore((state) => state.auth.user)
@@ -166,7 +175,7 @@ export function StatusGroupRow(props: {
</p>
</div>
)}
{astra && (
{(astra || props.detailsContent) && (
<Button
variant='ghost'
size='sm'
@@ -177,6 +186,7 @@ export function StatusGroupRow(props: {
<ArrowUpRight className='size-3.5' />
</Button>
)}
{props.actions}
</CardHeader>
{astra && (
<CardContent className='min-w-0 p-5'>
@@ -208,6 +218,7 @@ export function StatusGroupRow(props: {
>
<ArtworkThumbnail
id={work.id}
artworkBase={artworkBase}
title={t('Pelican sample {{number}}', {
number: index + 1,
})}
@@ -316,7 +327,7 @@ export function StatusGroupRow(props: {
<iframe
title={t('Pelican work preview')}
tabIndex={-1}
src={`/api/detections/artwork/${selectedWork.id}`}
src={artworkBase + selectedWork.id}
sandbox='allow-scripts'
referrerPolicy='no-referrer'
className='h-[65vh] w-full rounded-lg border bg-white'
@@ -328,7 +339,9 @@ export function StatusGroupRow(props: {
onOpenChange={setDetails}
title={`${monitor.name} · ${t('Detection details')}`}
>
<FingerprintPanel data={monitor.fingerprint} />
{props.detailsContent || (
<FingerprintPanel data={monitor.fingerprint} />
)}
</Dialog>
</Card>
)
@@ -345,7 +358,7 @@ export function GroupStatus() {
refetchInterval: 15_000,
})
const monitors = (query.data?.flatMap((group) => group.monitors) || []).sort(
(a, b) => Number(supportsAstra(b)) - Number(supportsAstra(a))
compareStatusMonitors
)
const overall = getOverallStatus(monitors.map((monitor) => monitor.status))
const operational = monitors.filter((monitor) => monitor.status === 1).length