From 36eea71c65e14b3c442653af78085dd64784ba6a Mon Sep 17 00:00:00 2001 From: Debian Date: Wed, 27 May 2026 20:51:34 +0200 Subject: [PATCH] fix(backends): safeID server name matching + routing priority swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- management-ui/src/pages/Backends/Detail.tsx | 9 ++++- .../src/pages/RoutingRules/index.tsx | 40 ++++++++++++++++++- 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 51f88d3..872610a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.119 +1.1.120 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index f01d3df..7342e07 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.119" +var version = "1.1.120" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 45fdc14..486fbab 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "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 diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 1344ce5..954227e 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -41,7 +41,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.119" +var version = "1.1.120" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/management-ui/src/pages/Backends/Detail.tsx b/management-ui/src/pages/Backends/Detail.tsx index 3be349d..570e930 100644 --- a/management-ui/src/pages/Backends/Detail.tsx +++ b/management-ui/src/pages/Backends/Detail.tsx @@ -85,6 +85,13 @@ function fmtBytes(n: number): string { 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() { const { t } = useTranslation() 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, render: (_, r) => { 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 const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red' diff --git a/management-ui/src/pages/RoutingRules/index.tsx b/management-ui/src/pages/RoutingRules/index.tsx index 18eb321..606b73c 100644 --- a/management-ui/src/pages/RoutingRules/index.tsx +++ b/management-ui/src/pages/RoutingRules/index.tsx @@ -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 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 { useTranslation } from 'react-i18next' import DataTable from '../../components/DataTable' @@ -135,6 +135,21 @@ export default function RoutingRulesPage() { 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 = [ { title: t('routing.domain'), dataIndex: 'domain_id', key: 'domain', render: (id: number) => domainName(id) }, { 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 ( + + +