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:
@@ -975,6 +975,18 @@
|
||||
"deleteConfirm": "ACL {{name}} wirklich löschen?",
|
||||
"emptyTitle": "Noch keine Forward-Proxy-ACLs.",
|
||||
"emptyDesc": "Default ohne ACLs: nur localnet (10/8, 172.16/12, 192.168/16) darf raus. Lege eine ACL an, um spezifische Domains/IPs/Ports gezielt zu erlauben oder zu blocken.",
|
||||
"statsCard": {
|
||||
"title": "Squid-Cache-Statistiken",
|
||||
"clientRequests": "Client-Anfragen",
|
||||
"cacheHits": "Cache-Treffer",
|
||||
"cacheHitPct": "Trefferquote",
|
||||
"clientErrors": "Client-Fehler",
|
||||
"bytesIn": "Empfangen",
|
||||
"bytesOut": "Gesendet",
|
||||
"serverRequests": "Backend-Anfragen",
|
||||
"serverErrors": "Backend-Fehler",
|
||||
"sinceRestart": "Seit letztem Neustart"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "allow — Zugriff erlauben",
|
||||
"deny": "deny — Zugriff blockieren"
|
||||
|
||||
@@ -975,6 +975,18 @@
|
||||
"deleteConfirm": "Really delete ACL {{name}}?",
|
||||
"emptyTitle": "No forward-proxy ACLs yet.",
|
||||
"emptyDesc": "Default with no ACLs: only localnet (10/8, 172.16/12, 192.168/16) is allowed out. Add an ACL to selectively allow or block specific domains/IPs/ports.",
|
||||
"statsCard": {
|
||||
"title": "Squid cache statistics",
|
||||
"clientRequests": "Client requests",
|
||||
"cacheHits": "Cache hits",
|
||||
"cacheHitPct": "Hit rate",
|
||||
"clientErrors": "Client errors",
|
||||
"bytesIn": "Bytes received",
|
||||
"bytesOut": "Bytes sent",
|
||||
"serverRequests": "Backend requests",
|
||||
"serverErrors": "Backend errors",
|
||||
"sinceRestart": "Since last restart"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "allow — permit access",
|
||||
"deny": "deny — block access"
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user