feat: per-Backend timeout server + Alerts-Deeplink + Retention + Cert-Prune — v1.3.4
- feat(backends): per-Backend `server_timeout_seconds` (nullable, Default 60s). Rendert `timeout server <N>s` im HAProxy-Backend-Block — für langsam antwortende Upstreams (KI-/Inferenz-Server mit gepufferter Antwort). Migration 0044 (+CHECK 1..86400), Model/Repo/Template/UI + Render-Test. - fix(ui): Dashboard-Alert-Karte verlinkt auf /alerts?tab=events; Alerts-Seite respektiert ?tab= (Deeplink landete bisher auf leerem Channels-Tab). - feat(scheduler): alert_events-Retention (90d) im täglichen Cleanup-Tick — Schutz vor unbounded growth der node-lokalen Health-Event-History. - fix(cluster): Cert-Sync prunt jetzt lokale .pem die der Primary nicht mehr hat (Waisen gelöschter Domains); schützt _default.pem + eigenen Node-Cert. - fix(security): x/text v0.37→v0.39 (GO-2026-5970, Infinite-Loop; via ACME+goose aktiv aufgerufen — govulncheck-Release-Gate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -592,6 +592,8 @@
|
||||
"websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
|
||||
"forceHttp1": "HTTP/1.1 erzwingen",
|
||||
"forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).",
|
||||
"serverTimeout": "Antwort-Timeout (timeout server)",
|
||||
"serverTimeoutHint": "Wie lange HAProxy auf die Antwort dieses Backends wartet, bevor es abbricht. Leer = Default (60s). Höher setzen für langsame Upstreams die NICHT streamen (z. B. KI-/Inferenz-Server mit gepufferter Antwort). Achtung: gilt als Inaktivitäts-Timeout — streamende Backends (SSE/chunked) brauchen das meist nicht.",
|
||||
"servers": "Server",
|
||||
"noServers": "kein Server",
|
||||
"noServersWarning": "Ein oder mehrere aktive Backends haben keine Server konfiguriert.",
|
||||
|
||||
@@ -592,6 +592,8 @@
|
||||
"websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
|
||||
"forceHttp1": "Force HTTP/1.1",
|
||||
"forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).",
|
||||
"serverTimeout": "Response timeout (timeout server)",
|
||||
"serverTimeoutHint": "How long HAProxy waits for this backend's response before aborting. Empty = default (60s). Raise it for slow upstreams that do NOT stream (e.g. AI/inference servers with a buffered response). Note: this is an inactivity timeout — streaming backends (SSE/chunked) usually don't need it.",
|
||||
"servers": "Servers",
|
||||
"noServers": "no server",
|
||||
"noServersWarning": "One or more active backends have no servers configured.",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
@@ -69,6 +70,14 @@ export default function AlertsPage() {
|
||||
const qc = useQueryClient()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
|
||||
// Tab über ?tab= steuerbar, damit der Dashboard-Deeplink
|
||||
// (/alerts?tab=events) direkt auf der Event-History landet statt auf
|
||||
// dem Channels-Tab. Default bleibt 'channels' für den Direktaufruf.
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const activeTab = searchParams.get('tab') === 'events' ? 'events' : 'channels'
|
||||
const setActiveTab = (key: string) =>
|
||||
setSearchParams(key === 'events' ? { tab: 'events' } : {}, { replace: true })
|
||||
|
||||
const channels = useQuery({
|
||||
queryKey: ['alerts', 'channels'],
|
||||
queryFn: async () => {
|
||||
@@ -304,7 +313,7 @@ export default function AlertsPage() {
|
||||
description={t('alerts.scopeDesc')}
|
||||
/>
|
||||
|
||||
<Tabs items={[
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[
|
||||
{
|
||||
key: 'channels',
|
||||
label: t('alerts.tabs.channels'),
|
||||
|
||||
@@ -20,13 +20,17 @@ interface Backend {
|
||||
id: number; name: string; scheme: string
|
||||
health_check_path?: string | null
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean; force_http1: boolean; active: boolean
|
||||
websocket: boolean; force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
}
|
||||
interface BackendFormValues {
|
||||
name: string; scheme: 'http' | 'https'
|
||||
health_check_path?: string
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean; force_http1: boolean; active: boolean
|
||||
websocket: boolean; force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
domain_ids?: number[]
|
||||
}
|
||||
interface BackendServer {
|
||||
@@ -174,6 +178,7 @@ export default function BackendDetailPage() {
|
||||
lb_algorithm: backend.lb_algorithm,
|
||||
websocket: backend.websocket,
|
||||
force_http1: backend.force_http1,
|
||||
server_timeout_seconds: backend.server_timeout_seconds ?? undefined,
|
||||
active: backend.active,
|
||||
domain_ids: attached.map(d => d.id),
|
||||
}}
|
||||
@@ -205,6 +210,11 @@ export default function BackendDetailPage() {
|
||||
extra={t('backends.forceHttp1Hint')}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('backends.serverTimeout')} name="server_timeout_seconds"
|
||||
extra={t('backends.serverTimeoutHint')}>
|
||||
<InputNumber min={1} max={86400} step={30}
|
||||
style={{ width: '100%' }} addonAfter="s" placeholder="60 (default)" />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('backends.active')} name="active" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface Backend {
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean
|
||||
force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -266,6 +267,7 @@ export default function BackendsPage() {
|
||||
<Space size={4}>
|
||||
<Tag>{v}</Tag>
|
||||
{row.websocket && <Tag color="cyan">WS</Tag>}
|
||||
{row.server_timeout_seconds ? <Tag color="gold">{row.server_timeout_seconds}s</Tag> : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -361,7 +361,7 @@ export default function DashboardPage() {
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
action={<Link to="/alerts" style={{ fontSize: 12 }}>{t('dashboard.alertsCard.viewAll')} →</Link>}
|
||||
action={<Link to="/alerts?tab=events" style={{ fontSize: 12 }}>{t('dashboard.alertsCard.viewAll')} →</Link>}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user