fix(backends): safeID server name matching + routing priority swap
Backend Detail page: HAProxy stat rows use safeID(name) as server token (spaces/dots → '_') but the UI matched on the raw DB name, so servers with non-alphanumeric names never showed live status. Added matching safeID helper in TypeScript (mirrors haproxy.go implementation). Routing Rules: commit priority up/down swap buttons (developed in a previous session, were left uncommitted). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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.119"
|
var version = "1.1.120"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -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.119"
|
var version = "1.1.120"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -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.119"
|
var version = "1.1.120"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ function fmtBytes(n: number): string {
|
|||||||
return n + ' B'
|
return n + ' B'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror of haproxy.go safeID: replaces any char outside [a-zA-Z0-9_-] with '_'.
|
||||||
|
// HAProxy uses this to generate server tokens; we need it to match stat rows.
|
||||||
|
function safeID(s: string): string {
|
||||||
|
const out = s.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||||
|
return out || 'unnamed'
|
||||||
|
}
|
||||||
|
|
||||||
export default function BackendDetailPage() {
|
export default function BackendDetailPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -316,7 +323,7 @@ function ServerPanel({ backendID, haproxyStats, isViewer }: { backendID: number;
|
|||||||
title: t('backends.server.live'), key: 'live', width: 160,
|
title: t('backends.server.live'), key: 'live', width: 160,
|
||||||
render: (_, r) => {
|
render: (_, r) => {
|
||||||
const stat = haproxyStats.find(
|
const stat = haproxyStats.find(
|
||||||
s => s.backend === `eg_backend_${backendID}` && s.server === r.name
|
s => s.backend === `eg_backend_${backendID}` && s.server === safeID(r.name)
|
||||||
)
|
)
|
||||||
if (!stat) return <Text type="secondary" style={{ fontSize: 11 }}>—</Text>
|
if (!stat) return <Text type="secondary" style={{ fontSize: 11 }}>—</Text>
|
||||||
const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red'
|
const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, message } from 'antd'
|
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, message } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { BranchesOutlined, PlusOutlined } from '@ant-design/icons'
|
import { ArrowDownOutlined, ArrowUpOutlined, BranchesOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
@@ -135,6 +135,21 @@ export default function RoutingRulesPage() {
|
|||||||
onError: (e: Error) => message.error(e.message),
|
onError: (e: Error) => message.error(e.message),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sortedRules = useMemo(
|
||||||
|
() => [...(rules ?? [])].sort((a, b) => a.priority - b.priority),
|
||||||
|
[rules],
|
||||||
|
)
|
||||||
|
|
||||||
|
const swapRules = useMutation({
|
||||||
|
mutationFn: async ({ a, b }: { a: RoutingRule; b: RoutingRule }) => {
|
||||||
|
const stripMeta = ({ id: _id, created_at: _ca, updated_at: _ua, ...r }: RoutingRule) => r
|
||||||
|
await apiClient.put(`/routing-rules/${a.id}`, { ...stripMeta(a), priority: b.priority })
|
||||||
|
await apiClient.put(`/routing-rules/${b.id}`, { ...stripMeta(b), priority: a.priority })
|
||||||
|
},
|
||||||
|
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['routing-rules'] }) },
|
||||||
|
onError: (e: Error) => message.error(e.message),
|
||||||
|
})
|
||||||
|
|
||||||
const columns: ColumnsType<RoutingRule> = [
|
const columns: ColumnsType<RoutingRule> = [
|
||||||
{ title: t('routing.domain'), dataIndex: 'domain_id', key: 'domain', render: (id: number) => domainName(id) },
|
{ title: t('routing.domain'), dataIndex: 'domain_id', key: 'domain', render: (id: number) => domainName(id) },
|
||||||
{ title: t('routing.pathPrefix'), dataIndex: 'path_prefix', key: 'path' },
|
{ title: t('routing.pathPrefix'), dataIndex: 'path_prefix', key: 'path' },
|
||||||
@@ -168,6 +183,27 @@ export default function RoutingRulesPage() {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '', key: 'move', width: 64,
|
||||||
|
render: (_, row) => {
|
||||||
|
const idx = sortedRules.findIndex(r => r.id === row.id)
|
||||||
|
const swapping = swapRules.isPending
|
||||||
|
return (
|
||||||
|
<Space size={2}>
|
||||||
|
<Tooltip title={t('fw.rule.moveUp')}>
|
||||||
|
<Button size="small" icon={<ArrowUpOutlined />}
|
||||||
|
disabled={isViewer || idx <= 0 || swapping}
|
||||||
|
onClick={() => swapRules.mutate({ a: row, b: sortedRules[idx - 1] })} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t('fw.rule.moveDown')}>
|
||||||
|
<Button size="small" icon={<ArrowDownOutlined />}
|
||||||
|
disabled={isViewer || idx >= sortedRules.length - 1 || swapping}
|
||||||
|
onClick={() => swapRules.mutate({ a: row, b: sortedRules[idx + 1] })} />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('common.actions'), key: 'actions',
|
title: t('common.actions'), key: 'actions',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user