feat(fwd-proxy): Squid cache stats card — GET /forward-proxy/stats + squidclient mgr:counters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-25 16:47:31 +02:00
parent f29d74bad7
commit 711c4c7eb1
8 changed files with 176 additions and 7 deletions

View File

@@ -1,9 +1,9 @@
import { useMemo, useState } from 'react'
import {
Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, Typography, message,
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ArrowDownOutlined, ArrowUpOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined } from '@ant-design/icons'
import { ArrowDownOutlined, ArrowUpOutlined, BarChartOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
@@ -52,6 +52,24 @@ async function listACLs(): Promise<ACL[]> {
return (r.data.data as { acls?: ACL[] }).acls ?? []
}
interface SquidStats {
client_requests: number
cache_hits: number
cache_hit_pct: number
client_errors: number
bytes_in: number
bytes_out: number
server_requests: number
server_errors: number
}
function fmtBytes(n: number): string {
if (n >= 1_073_741_824) return (n / 1_073_741_824).toFixed(1) + ' GB'
if (n >= 1_048_576) return (n / 1_048_576).toFixed(1) + ' MB'
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
return n + ' B'
}
export default function ForwardProxyPage() {
const { t } = useTranslation()
const qc = useQueryClient()
@@ -68,6 +86,18 @@ export default function ForwardProxyPage() {
})
const squid = services?.find(s => s.unit === 'squid.service' || s.unit === 'squid')
const { data: statsData, refetch: refetchStats } = useQuery({
queryKey: ['fwd-proxy', 'stats'],
queryFn: async () => {
const r = await apiClient.get('/forward-proxy/stats')
if (!isEnvelope(r.data)) return null
const d = r.data.data as { stats?: SquidStats; error?: string }
if (d.error) return null
return d.stats ?? null
},
refetchInterval: 30_000,
})
const [editing, setEditing] = useState<ACL | null>(null)
const [creating, setCreating] = useState(false)
const [form] = Form.useForm<FormValues>()
@@ -206,6 +236,54 @@ export default function ForwardProxyPage() {
description={t('fwd.helpBody')}
/>
{statsData && (
<Card
size="small"
className="mb-12"
title={<><BarChartOutlined /> {t('fwd.statsCard.title')}</>}
extra={
<Space size={4}>
<Typography.Text type="secondary" style={{ fontSize: 11 }}>{t('fwd.statsCard.sinceRestart')}</Typography.Text>
<Tooltip title={t('common.refresh')}>
<Button size="small" icon={<ReloadOutlined />} onClick={() => { void refetchStats() }} />
</Tooltip>
</Space>
}
>
<Row gutter={[16, 8]}>
<Col xs={12} sm={6} md={4}>
<Statistic title={t('fwd.statsCard.clientRequests')} value={statsData.client_requests} />
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic title={t('fwd.statsCard.cacheHits')} value={statsData.cache_hits} />
</Col>
<Col xs={12} sm={6} md={4}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{t('fwd.statsCard.cacheHitPct')}</Typography.Text>
<Progress
percent={Math.round(statsData.cache_hit_pct)}
size="small"
strokeColor={statsData.cache_hit_pct >= 50 ? '#16a34a' : statsData.cache_hit_pct >= 20 ? '#ca8a04' : '#dc2626'}
/>
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic title={t('fwd.statsCard.serverRequests')} value={statsData.server_requests} />
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic title={t('fwd.statsCard.bytesIn')} value={fmtBytes(statsData.bytes_in)} />
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic title={t('fwd.statsCard.bytesOut')} value={fmtBytes(statsData.bytes_out)} />
</Col>
</Row>
{statsData.client_errors > 0 && (
<Typography.Text type="danger" style={{ fontSize: 12 }}>
{t('fwd.statsCard.clientErrors')}: {statsData.client_errors}
{statsData.server_errors > 0 && ` · ${t('fwd.statsCard.serverErrors')}: ${statsData.server_errors}`}
</Typography.Text>
)}
</Card>
)}
<DataTable
rowKey="id"
loading={isLoading}