fix(cluster): Repair-Rollenerkennung über pg_publication statt ha_nodes.role — v1.2.88

Bug: Dispatch an utm-2 schlug fehl ('dieser Node ist der Cluster-Primary'), weil ha_nodes je Node lokal ist und sich JEDE Node selbst als role=primary markiert. Fix: Primary-Erkennung über pg_publication (edgeguard_shared, für jeden DB-User lesbar) statt role/pg_role. Primary gibt dem Subscriber seine eigene Adresse als primary_host mit (PostPeerWithBody); Agent-Handler vertraut dem mTLS-Dispatch mit Safety-Guard 'läuft nie auf dem Publication-Primary'. Funktioniert auch bei Direktzugriff auf den Subscriber. UI-Gating vereinfacht (Drift + Peer + Admin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-04 13:18:54 +02:00
parent 66c71c5fa8
commit 90f0df4c45
3 changed files with 121 additions and 115 deletions

View File

@@ -392,17 +392,13 @@ export default function ClusterPage() {
const primaryFqdn = data?.local_node?.fqdn ?? window.location.hostname
// Repair-Button: sichtbar bei Drift, für Admins, wenn ein Resync-Ziel
// existiert — auf dem Standby (lokal) oder auf dem Primary (delegiert
// an den Subscriber-Peer). Primary = role ODER pg_role 'primary'
// (pg_role bleibt nach setup-standby 'standalone', role ist verlässlich).
const isPrimaryNode = (n?: HANode | null) => !!n && (n.pg_role === 'primary' || n.role === 'primary')
const localIsPrimary = isPrimaryNode(data?.local_node)
const hasSubscriberPeer = data?.peers?.some(p => !isPrimaryNode(p)) ?? false
const hasPrimary = localIsPrimary || (data?.peers?.some(isPrimaryNode) ?? false)
// Repair-Button: sichtbar bei Drift, für Admins, sobald ein Peer
// existiert. Welche Node Primary (Publication-Quelle) bzw. Subscriber
// ist, entscheidet das Backend zur Laufzeit über pg_publication — die
// UI muss das nicht raten (ha_nodes.role ist je Node lokal/unzuverlässig).
const canRepair = !isViewer
&& !!data?.drift_found
&& (localIsPrimary ? hasSubscriberPeer : hasPrimary)
&& ((data?.peers?.length ?? 0) > 0)
const peerColumns: ColumnsType<HANode> = [
{
@@ -540,13 +536,7 @@ export default function ClusterPage() {
banner
className="mb-16"
message={t('cluster.driftBanner')}
description={
<>
<Paragraph style={{ marginBottom: 8 }}>{t('cluster.driftBannerDesc')}</Paragraph>
{localIsPrimary && !hasSubscriberPeer
&& <Text type="secondary">{t('cluster.repair.noStandbyHint')}</Text>}
</>
}
description={t('cluster.driftBannerDesc')}
action={
canRepair ? (
<Popconfirm