diff --git a/VERSION b/VERSION
index 032f5c7..b59e8c3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.145
+1.1.146
diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go
index 4c29bed..2955841 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.145"
+var version = "1.1.146"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")
diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go
index 00f09e8..d81a8ec 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.145"
+var version = "1.1.146"
const usage = `edgeguard-ctl — EdgeGuard CLI
diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go
index f737b1f..a32a22e 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.145"
+var version = "1.1.146"
const (
// renewTickInterval — how often we re-evaluate expiring certs.
diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json
index b4f45df..b97fefd 100644
--- a/management-ui/src/i18n/locales/de/common.json
+++ b/management-ui/src/i18n/locales/de/common.json
@@ -448,7 +448,19 @@
"noSelf": "Selbst-Registrierung in ha_nodes fehlgeschlagen — Setup-Wizard durchlaufen.",
"peersTitle": "Peers ({{count}})",
"singleNodeTitle": "Single-Node-Modus",
- "singleNodeDesc": "Nur diese Box bekannt. Zusätzlicher Knoten? `edgeguard-ctl cluster-join` (kommt in Phase 3.2).",
+ "singleNodeDesc": "Nur diese Box bekannt. Folge den Schritten unten um einen zweiten Node beizutreten.",
+ "addNodeTitle": "Zweiten Node dem Cluster hinzufügen",
+ "step1Title": "1 — EdgeGuard auf dem neuen Host installieren",
+ "step1Desc": "Als root auf dem neuen Host ausführen (Debian 13, amd64):",
+ "step2Title": "2 — Join-Token hier generieren (auf diesem Primary-Node)",
+ "step2Desc": "Erzeugt einen einmaligen HMAC-Token, der 30 Minuten gültig ist. Der Token autorisiert den neuen Node ein signiertes Cluster-TLS-Zertifikat zu erhalten.",
+ "step3Title": "3 — Auf dem neuen Node ausführen",
+ "step3NoToken": "Zuerst oben auf \"Join-Token erzeugen\" klicken — der vollständige Befehl erscheint dann hier.",
+ "step4Title": "4 — edgeguard-api auf dem neuen Node neu starten",
+ "step4Desc": "Der Dienst lädt das neue Cluster-TLS-Zertifikat und beginnt Heartbeats zu senden. Der Node erscheint innerhalb von 30 Sekunden in der Peers-Tabelle.",
+ "apiUnreachable": "Cluster-API nicht erreichbar",
+ "tokenLabel": "Token",
+ "caFingerprintLabel": "CA-Fingerabdruck",
"driftBanner": "Config-Drift erkannt",
"driftBannerDesc": "Ein oder mehrere Peers haben einen anderen Config-Hash als dieser Node. Entweder stehen noch Änderungen in der Outbox, oder auf einem Peer wurde direkt in der DB editiert. Warte bis die Outbox leer ist oder starte Diagnostics.",
"col": {
@@ -1069,6 +1081,7 @@
"download": "Download",
"copy": "Kopieren",
"copied": "Kopiert",
+ "retry": "Erneut versuchen",
"close": "Schließen",
"refresh": "Aktualisieren",
"up": "UP",
diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json
index 29efae7..7ef7944 100644
--- a/management-ui/src/i18n/locales/en/common.json
+++ b/management-ui/src/i18n/locales/en/common.json
@@ -448,7 +448,19 @@
"noSelf": "Self-registration in ha_nodes failed — run the setup wizard.",
"peersTitle": "Peers ({{count}})",
"singleNodeTitle": "Single-node mode",
- "singleNodeDesc": "Only this box is known. To add another node: `edgeguard-ctl cluster-join` (coming in phase 3.2).",
+ "singleNodeDesc": "Only this box is known. Follow the steps below to join a second node.",
+ "addNodeTitle": "Add second node to cluster",
+ "step1Title": "1 — Install EdgeGuard on the new host",
+ "step1Desc": "Run as root on the new host (Debian 13, amd64):",
+ "step2Title": "2 — Generate a join token here (on this primary node)",
+ "step2Desc": "Creates a one-time HMAC token valid for 30 minutes. The token authorises the new node to receive a signed cluster TLS certificate.",
+ "step3Title": "3 — Run on the new node",
+ "step3NoToken": "Click \"Generate join token\" above — the full command will appear here.",
+ "step4Title": "4 — Restart edgeguard-api on the new node",
+ "step4Desc": "The service loads the new cluster TLS certificate and starts sending heartbeats. The node appears in the Peers table within 30 seconds.",
+ "apiUnreachable": "Cluster API not reachable",
+ "tokenLabel": "Token",
+ "caFingerprintLabel": "CA fingerprint",
"driftBanner": "Config drift detected",
"driftBannerDesc": "One or more peers have a different config hash than this node. Either changes are still in the outbox or a peer was edited directly in the DB. Wait for the outbox to drain or run diagnostics.",
"col": {
@@ -1069,6 +1081,7 @@
"download": "Download",
"copy": "Copy",
"copied": "Copied",
+ "retry": "Retry",
"close": "Close",
"refresh": "Refresh",
"up": "UP",
diff --git a/management-ui/src/pages/Cluster/index.tsx b/management-ui/src/pages/Cluster/index.tsx
index e3e8607..e439d10 100644
--- a/management-ui/src/pages/Cluster/index.tsx
+++ b/management-ui/src/pages/Cluster/index.tsx
@@ -1,6 +1,6 @@
-import { Alert, Button, Card, Descriptions, Input, Modal, Popconfirm, Space, Spin, Table, Tag, Tooltip, Typography, message } from 'antd'
+import { Alert, Button, Card, Descriptions, Input, Popconfirm, Space, Spin, Table, Tag, Tooltip, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
-import { ApartmentOutlined, CopyOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons'
+import { ApartmentOutlined, CopyOutlined, DeleteOutlined, KeyOutlined, ReloadOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -9,7 +9,9 @@ import apiClient, { isEnvelope } from '../../api/client'
import PageHeader from '../../components/PageHeader'
import { useAuthStore } from '../../stores/auth'
-const { Text } = Typography
+const { Text, Paragraph } = Typography
+
+const INSTALL_ONELINER = 'curl -fsSL https://get.netcell-edgeguard.de | sudo bash'
interface HANode {
id: string
@@ -102,9 +104,6 @@ function lastSeenRelative(iso: string | null | undefined, now: number): string {
return `${Math.round(ms / 3_600_000)}h`
}
-// useTickingNow gibt einen `now`-Wert zurück der jede Sekunde
-// re-rendert. Damit tickt das Cluster-Last-Seen-Label visuell jede
-// Sekunde, statt nur alle 30s beim useQuery-Refetch.
function useTickingNow(intervalMs = 1000): number {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
@@ -114,24 +113,110 @@ function useTickingNow(intervalMs = 1000): number {
return now
}
+function formatUptime(sec: number): string {
+ if (!sec || sec < 0) return '—'
+ const d = Math.floor(sec / 86400)
+ const h = Math.floor((sec % 86400) / 3600)
+ const m = Math.floor((sec % 3600) / 60)
+ if (d > 0) return `${d}d ${h}h`
+ if (h > 0) return `${h}h ${m}m`
+ return `${m}m`
+}
+
+function CertExpiry({ days, until }: { days: number; until: string }) {
+ const { t } = useTranslation()
+ const color = days < 0 ? 'red' : days < 30 ? 'red' : days < 90 ? 'orange' : 'green'
+ const label = days < 0
+ ? t('cluster.certExpiredDaysAgo', { n: -days })
+ : t('cluster.certDaysRemaining', { n: days })
+ return (
+
+ {label}
+
+ )
+}
+
+// Copyable code block — monospace TextArea + copy button below
+function CopyCode({ value }: { value: string }) {
+ const { t } = useTranslation()
+ return (
+