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 (
+
+
+ }
+ disabled={isViewer || idx <= 0 || swapping}
+ onClick={() => swapRules.mutate({ a: row, b: sortedRules[idx - 1] })} />
+
+
+ }
+ disabled={isViewer || idx >= sortedRules.length - 1 || swapping}
+ onClick={() => swapRules.mutate({ a: row, b: sortedRules[idx + 1] })} />
+
+
+ )
+ },
+ },
{
title: t('common.actions'), key: 'actions',
render: (_, row) => (