fix(alerts+diag): quickToggle SMTP-Datenverlust + Diagnostics Viewer-Gate

Alerts: quickToggle schickte den Channel durch buildPayload(), das SMTP-
Settings aus einem flachen ChannelFormValues rekonstruiert. Bei Email-
Channels wurde damit jedes aktiv/inaktiv-Toggle zum SMTP-Config-Wipe,
weil die Flat-Fields (smtp_host, smtp_port, from, ...) undefined waren.
Fix: raw Channel-Objekt mit geändertem active-Flag direkt PUT-ten.

testFire-Button fehlte disabled={isViewer} + Tooltip.

Diagnostics: kein isViewer-Check, Viewer sahen funktionierende Run-
Buttons und bekamen 403 ohne Erklärung. Fix: isViewer aus Auth-Store,
disabled-Prop in ToolCard, alle 5 Cards + Enter-Handler gegattet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-28 14:44:58 +02:00
parent d87068535d
commit 1c39a0e533
6 changed files with 33 additions and 20 deletions

View File

@@ -1 +1 @@
1.1.126 1.1.127

View File

@@ -60,7 +60,7 @@ import (
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
) )
var version = "1.1.126" var version = "1.1.127"
func main() { func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR") addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

@@ -11,7 +11,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
) )
var version = "1.1.126" var version = "1.1.127"
const usage = `edgeguard-ctl — EdgeGuard CLI const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -41,7 +41,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
) )
var version = "1.1.126" var version = "1.1.127"
const ( const (
// renewTickInterval — how often we re-evaluate expiring certs. // renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -147,7 +147,7 @@ export default function AlertsPage() {
}) })
const quickToggle = useMutation({ const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: Channel; checked: boolean }) => { mutationFn: async ({ id, row, checked }: { id: number; row: Channel; checked: boolean }) => {
await apiClient.put(`/alerts/channels/${id}`, buildPayload({ ...row, active: checked } as ChannelFormValues)) await apiClient.put(`/alerts/channels/${id}`, { ...row, active: checked })
}, },
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['alerts', 'channels'] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['alerts', 'channels'] }) },
onError: (e: Error) => message.error(e.message), onError: (e: Error) => message.error(e.message),
@@ -284,11 +284,14 @@ export default function AlertsPage() {
subtitle={t('alerts.intro')} subtitle={t('alerts.intro')}
extra={ extra={
<Space> <Space>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button icon={<ExperimentOutlined />} <Button icon={<ExperimentOutlined />}
onClick={() => testFire.mutate()} onClick={() => testFire.mutate()}
loading={testFire.isPending}> loading={testFire.isPending}
disabled={isViewer}>
{t('alerts.test')} {t('alerts.test')}
</Button> </Button>
</Tooltip>
</Space> </Space>
} }
/> />

View File

@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'
import apiClient, { isEnvelope } from '../../api/client' import apiClient, { isEnvelope } from '../../api/client'
import PageHeader from '../../components/PageHeader' import PageHeader from '../../components/PageHeader'
import { useAuthStore } from '../../stores/auth'
const { Text } = Typography const { Text } = Typography
@@ -30,7 +31,7 @@ interface DiagResult {
// - Eigene Input-Form // - Eigene Input-Form
// - "Ausführen"-Button → POST /diagnostics/<tool> // - "Ausführen"-Button → POST /diagnostics/<tool>
// - Output-Pane mit monospace-Output, Status-Tag (OK/Error), Dauer // - Output-Pane mit monospace-Output, Status-Tag (OK/Error), Dauer
function ToolCard({ icon, title, intro, children, result, loading, run }: { function ToolCard({ icon, title, intro, children, result, loading, run, disabled }: {
icon: React.ReactNode icon: React.ReactNode
title: string title: string
intro: string intro: string
@@ -38,6 +39,7 @@ function ToolCard({ icon, title, intro, children, result, loading, run }: {
result: DiagResult | null result: DiagResult | null
loading: boolean loading: boolean
run: () => void run: () => void
disabled?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
@@ -45,10 +47,12 @@ function ToolCard({ icon, title, intro, children, result, loading, run }: {
size="small" size="small"
title={<Space>{icon}<Text strong>{title}</Text></Space>} title={<Space>{icon}<Text strong>{title}</Text></Space>}
extra={ extra={
<Tooltip title={disabled ? t('auth.viewerBadge') : undefined}>
<Button type="primary" size="small" onClick={run} loading={loading} <Button type="primary" size="small" onClick={run} loading={loading}
icon={<RadarChartOutlined />}> disabled={disabled} icon={<RadarChartOutlined />}>
{t('diag.run')} {t('diag.run')}
</Button> </Button>
</Tooltip>
} }
> >
<Text type="secondary" style={{ fontSize: 12 }}>{intro}</Text> <Text type="secondary" style={{ fontSize: 12 }}>{intro}</Text>
@@ -84,6 +88,7 @@ function ToolCard({ icon, title, intro, children, result, loading, run }: {
export default function DiagnosticsPage() { export default function DiagnosticsPage() {
const { t } = useTranslation() const { t } = useTranslation()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const [pingTarget, setPingTarget] = useState('1.1.1.1') const [pingTarget, setPingTarget] = useState('1.1.1.1')
const [traceTarget, setTraceTarget] = useState('1.1.1.1') const [traceTarget, setTraceTarget] = useState('1.1.1.1')
@@ -145,11 +150,12 @@ export default function DiagnosticsPage() {
result={pingRes} result={pingRes}
loading={ping.isPending} loading={ping.isPending}
run={() => ping.mutate()} run={() => ping.mutate()}
disabled={isViewer}
> >
<Input placeholder="1.1.1.1 oder example.com" <Input placeholder="1.1.1.1 oder example.com"
value={pingTarget} value={pingTarget}
onChange={(e) => setPingTarget(e.target.value)} onChange={(e) => setPingTarget(e.target.value)}
onPressEnter={() => ping.mutate()} /> onPressEnter={() => { if (!isViewer) ping.mutate() }} />
</ToolCard> </ToolCard>
</Col> </Col>
<Col xs={24} lg={12}> <Col xs={24} lg={12}>
@@ -160,11 +166,12 @@ export default function DiagnosticsPage() {
result={traceRes} result={traceRes}
loading={trace.isPending} loading={trace.isPending}
run={() => trace.mutate()} run={() => trace.mutate()}
disabled={isViewer}
> >
<Input placeholder="1.1.1.1 oder example.com" <Input placeholder="1.1.1.1 oder example.com"
value={traceTarget} value={traceTarget}
onChange={(e) => setTraceTarget(e.target.value)} onChange={(e) => setTraceTarget(e.target.value)}
onPressEnter={() => trace.mutate()} /> onPressEnter={() => { if (!isViewer) trace.mutate() }} />
</ToolCard> </ToolCard>
</Col> </Col>
<Col xs={24} lg={12}> <Col xs={24} lg={12}>
@@ -175,12 +182,13 @@ export default function DiagnosticsPage() {
result={digRes} result={digRes}
loading={dig.isPending} loading={dig.isPending}
run={() => dig.mutate()} run={() => dig.mutate()}
disabled={isViewer}
> >
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
<Input placeholder="netcell-it.de" <Input placeholder="netcell-it.de"
value={digName} value={digName}
onChange={(e) => setDigName(e.target.value)} onChange={(e) => setDigName(e.target.value)}
onPressEnter={() => dig.mutate()} onPressEnter={() => { if (!isViewer) dig.mutate() }}
style={{ flex: 1 }} /> style={{ flex: 1 }} />
<Select <Select
value={digType} value={digType}
@@ -199,11 +207,12 @@ export default function DiagnosticsPage() {
result={curlRes} result={curlRes}
loading={curl.isPending} loading={curl.isPending}
run={() => curl.mutate()} run={() => curl.mutate()}
disabled={isViewer}
> >
<Input placeholder="https://example.com" <Input placeholder="https://example.com"
value={curlUrl} value={curlUrl}
onChange={(e) => setCurlUrl(e.target.value)} onChange={(e) => setCurlUrl(e.target.value)}
onPressEnter={() => curl.mutate()} /> onPressEnter={() => { if (!isViewer) curl.mutate() }} />
</ToolCard> </ToolCard>
</Col> </Col>
<Col xs={24}> <Col xs={24}>
@@ -214,12 +223,13 @@ export default function DiagnosticsPage() {
result={tcpRes} result={tcpRes}
loading={tcp.isPending} loading={tcp.isPending}
run={() => tcp.mutate()} run={() => tcp.mutate()}
disabled={isViewer}
> >
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
<Input placeholder="10.0.5.14" <Input placeholder="10.0.5.14"
value={tcpHost} value={tcpHost}
onChange={(e) => setTcpHost(e.target.value)} onChange={(e) => setTcpHost(e.target.value)}
onPressEnter={() => tcp.mutate()} onPressEnter={() => { if (!isViewer) tcp.mutate() }}
style={{ flex: 1 }} /> style={{ flex: 1 }} />
<InputNumber <InputNumber
min={1} max={65535} min={1} max={65535}