diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json
index 9ae7987..ceec102 100644
--- a/management-ui/src/i18n/locales/de/common.json
+++ b/management-ui/src/i18n/locales/de/common.json
@@ -424,8 +424,9 @@
"split-brain": "split-brain"
},
"roleDbPrimary": "DB-Primary",
- "rolePeer": "Peer",
- "roleHint": "DB-/Cluster-Rolle: bestimmt, wohin Schreibzugriffe gehen. Sie wandert NICHT mit der VIP — ein Node kann DB-Primary sein und trotzdem gerade keepalived-BACKUP (siehe VIP/VRRP-Karte). Sie aendert sich nur durch „edgeguard-ctl promote“."
+ "roleHint": "Datenbank-Rolle aus der Replikation: DB-Primary nimmt Schreibzugriffe entgegen, DB-Standby repliziert von dort. Sie wandert NICHT mit der VIP — ein Knoten kann DB-Primary sein und trotzdem gerade keepalived-BACKUP (siehe VIP/VRRP-Karte). Sie aendert sich nur durch „edgeguard-ctl promote“.",
+ "roleDbStandby": "DB-Standby",
+ "roleDbUnknown": "—"
},
"routingCard": {
"title": "Routing",
diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json
index f564d4e..382d6d4 100644
--- a/management-ui/src/i18n/locales/en/common.json
+++ b/management-ui/src/i18n/locales/en/common.json
@@ -424,8 +424,9 @@
"split-brain": "split-brain"
},
"roleDbPrimary": "DB primary",
- "rolePeer": "Peer",
- "roleHint": "Database/cluster role: decides where writes go. It does NOT follow the VIP — a node can be DB primary while currently being keepalived BACKUP (see the VIP/VRRP card). It only changes via \"edgeguard-ctl promote\"."
+ "roleHint": "Database role from replication: the DB primary accepts writes, the DB standby replicates from it. It does NOT follow the VIP — a node can be DB primary while currently being keepalived BACKUP (see the VIP/VRRP card). It only changes via \"edgeguard-ctl promote\".",
+ "roleDbStandby": "DB standby",
+ "roleDbUnknown": "—"
},
"routingCard": {
"title": "Routing",
diff --git a/management-ui/src/pages/Dashboard/index.tsx b/management-ui/src/pages/Dashboard/index.tsx
index fb1075c..c7edae5 100644
--- a/management-ui/src/pages/Dashboard/index.tsx
+++ b/management-ui/src/pages/Dashboard/index.tsx
@@ -68,7 +68,7 @@ interface FwRule { id: number; enabled: boolean; action: string }
interface FwNAT { id: number; enabled: boolean; kind: string }
interface FwZone { id: number; name: string; builtin: boolean }
interface TLSCert { id: number; common_name: string; not_after?: string }
-interface ClusterNode { id: string; fqdn: string; role: string }
+interface ClusterNode { id: string; fqdn: string; role: string; pg_role?: string }
interface WGIface { id: number; name: string; mode: string; active: boolean }
interface WGStatusRow {
interface: string
@@ -644,6 +644,24 @@ function VIPCard({ data }: { data?: VIPStatus | null }) {
// ── Cluster card ──────────────────────────────────────────────
+// dbRoleLabel/-Color bilden pg_role ab. 'standalone' bzw. leer heisst:
+// keine Replikation eingerichtet — dann gibt es schlicht keine DB-Rolle
+// zu zeigen, statt eine zu erfinden.
+function dbRoleLabel(pgRole: string | undefined, t: (k: string) => string): string {
+ switch (pgRole) {
+ case 'primary': return t('dashboard.clusterCard.roleDbPrimary')
+ case 'standby': return t('dashboard.clusterCard.roleDbStandby')
+ default: return t('dashboard.clusterCard.roleDbUnknown')
+ }
+}
+function dbRoleColor(pgRole: string | undefined): string {
+ switch (pgRole) {
+ case 'primary': return 'green'
+ case 'standby': return 'blue'
+ default: return 'default'
+ }
+}
+
interface ClusterStatusCardProps {
nodes: ClusterNode[]
status: { mode: string; health: string; drift_found: boolean } | null
@@ -677,16 +695,19 @@ function ClusterStatusCard({ nodes, status }: ClusterStatusCardProps) {
padding: '4px 0', borderBottom: '1px solid #F1F5F9', fontSize: 12,
}}>
{n.fqdn}
- {/* ha_nodes.role ist die DB-/Cluster-Rolle (wohin Schreibzugriffe
- gehen) und wandert bewusst NICHT mit der VIP — sie aendert
- sich nur durch `edgeguard-ctl promote`. Ein nacktes "primary"
- hier las sich neben der VIP-Karte ("BACKUP") wie ein
- Widerspruch, deshalb explizit als DB-Rolle beschriftet. */}
+ {/* Bewusst pg_role, NICHT role: ha_nodes ist node-lokal (nicht
+ repliziert), und jeder Node traegt sich in `role` selbst ein —
+ ein per Join dazugekommener Node behaelt dort den Default
+ "primary" und behauptete deshalb in seiner eigenen Ansicht,
+ beide Knoten seien DB-Primary. `pg_role` folgt der
+ tatsaechlichen Replikationsrolle und stimmt auf beiden Seiten
+ ueberein. Siehe auch cluster_repair.go: role/pg_role sind
+ node-lokal, verlaesslich ist letztlich die PUBLICATION.
+ Die DB-Rolle wandert NICHT mit der VIP — sie aendert sich nur
+ durch `edgeguard-ctl promote`. */}
-
- {n.role === 'primary'
- ? t('dashboard.clusterCard.roleDbPrimary')
- : t('dashboard.clusterCard.rolePeer')}
+
+ {dbRoleLabel(n.pg_role, t)}