fix(update): self-upgrade via sudo systemd-run + animiertes Modal
handler: edgeguard-User darf systemd-run nicht direkt aufrufen ("Inter-
active authentication required"). sudo -n + sudoers-Whitelist auf
exakt die Unit-Form für edgeguard-upgrade.service.
UI: UpdateBanner-Komponente neu — Pattern wie mail-gateway/enconf:
Banner mit Force-Check-Button + Popconfirm. Beim Apply zeigt full-
screen-Overlay mit animiertem Orbit (zwei Ringe + Dots), Versions-
sprung, vier Step-Indicators (Download/Install/Restart/Verify) und
Live-Timer. Poll auf /system/health detektiert Version-Flip ODER
"sah down dann up" und window.reload nach 1.5s. Sicherheits-Timeout
2 min schickt sonst auch reload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ import (
|
|||||||
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.51"
|
var version = "1.0.52"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.51"
|
var version = "1.0.52"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.51"
|
var version = "1.0.52"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -261,14 +261,22 @@ rm -f /tmp/edgeguard-upgrade.sh
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unitName = "edgeguard-upgrade.service"
|
const unitName = "edgeguard-upgrade.service"
|
||||||
_ = exec.Command("systemctl", "reset-failed", unitName).Run()
|
// API läuft als edgeguard-User; systemd-run + systemctl reset-failed
|
||||||
cmd := exec.Command("systemd-run",
|
// brauchen root. Sudoers-Whitelist in postinst lässt exakt diese
|
||||||
|
// beiden Aufrufe durch. Ohne sudo schlug das früher mit
|
||||||
|
// "Interactive authentication required" fehl und der Fallback
|
||||||
|
// (setsid bash als edgeguard) konnte kein apt-get update — das
|
||||||
|
// Modal blieb hängen und die Box nicht aktualisiert.
|
||||||
|
_ = exec.Command("sudo", "-n", "/usr/bin/systemctl", "reset-failed", unitName).Run()
|
||||||
|
cmd := exec.Command("sudo", "-n", "/usr/bin/systemd-run",
|
||||||
"--unit="+unitName,
|
"--unit="+unitName,
|
||||||
"--description=EdgeGuard self-upgrade",
|
"--description=EdgeGuard self-upgrade",
|
||||||
"--collect",
|
"--collect",
|
||||||
"bash", "/tmp/edgeguard-upgrade.sh")
|
"bash", "/tmp/edgeguard-upgrade.sh")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
// systemd-run unavailable (dev env) — fall back to setsid
|
// systemd-run unavailable (dev env without sudo) — fall back
|
||||||
|
// to setsid. In Prod sollte das nie greifen.
|
||||||
|
slog.Warn("upgrade: sudo systemd-run failed, falling back to setsid", "error", err)
|
||||||
fallback := exec.Command("setsid", "bash", "/tmp/edgeguard-upgrade.sh")
|
fallback := exec.Command("setsid", "bash", "/tmp/edgeguard-upgrade.sh")
|
||||||
fallback.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
fallback.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
if err2 := fallback.Start(); err2 != nil {
|
if err2 := fallback.Start(); err2 != nil {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const NAV: NavSection[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const VERSION = '1.0.51'
|
const VERSION = '1.0.52'
|
||||||
|
|
||||||
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
import { Alert, Button, List, Modal, Tag, Typography, message } from 'antd'
|
import { Alert, Button, Popconfirm, Space, message } from 'antd'
|
||||||
import { CloudDownloadOutlined } from '@ant-design/icons'
|
import { CloudDownloadOutlined, ReloadOutlined, RocketOutlined } from '@ant-design/icons'
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../api/client'
|
import apiClient, { isEnvelope } from '../api/client'
|
||||||
|
|
||||||
const { Text } = Typography
|
interface PackageVersions { [key: string]: string }
|
||||||
|
interface SystemHealth { status: string; version: string }
|
||||||
|
|
||||||
interface PackageVersions {
|
interface PendingUpdate { pkg: string; installed: string; available: string }
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PendingUpdate {
|
// allUpdates parsed das flache map-Format ({pkg_installed,pkg_available})
|
||||||
pkg: string
|
// das /system/package-versions zurückliefert. Eines davon ist meist
|
||||||
installed: string
|
// das meta-Paket "edgeguard" → die "Ziel-Version".
|
||||||
available: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// allUpdates lists every package with installed != available.
|
|
||||||
// Match enconf/mail-gateway pattern of showing the multi-package
|
|
||||||
// list in a confirm-modal rather than just one banner-line.
|
|
||||||
function allUpdates(v: PackageVersions): PendingUpdate[] {
|
function allUpdates(v: PackageVersions): PendingUpdate[] {
|
||||||
const out: PendingUpdate[] = []
|
const out: PendingUpdate[] = []
|
||||||
for (const key of Object.keys(v)) {
|
for (const key of Object.keys(v)) {
|
||||||
@@ -35,141 +28,209 @@ function allUpdates(v: PackageVersions): PendingUpdate[] {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemHealth { status: string; version: string }
|
|
||||||
|
|
||||||
export default function UpdateBanner() {
|
export default function UpdateBanner() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [msg, msgCtx] = message.useMessage()
|
||||||
// applyingFor holds the version we expect AFTER the upgrade so the
|
|
||||||
// modal can poll system/health and detect the version-flip. Switches
|
|
||||||
// back to null + closes the modal once the new version is live.
|
|
||||||
const [applyingFor, setApplyingFor] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const { data } = useQuery({
|
const pkgVersions = useQuery({
|
||||||
queryKey: ['system', 'package-versions'],
|
queryKey: ['system', 'package-versions'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const r = await apiClient.get('/system/package-versions')
|
const r = await apiClient.get('/system/package-versions')
|
||||||
if (isEnvelope(r.data)) return r.data.data as PackageVersions
|
return isEnvelope(r.data) ? (r.data.data as PackageVersions) : ({} as PackageVersions)
|
||||||
return {} as PackageVersions
|
|
||||||
},
|
},
|
||||||
refetchInterval: 5 * 60 * 1000,
|
refetchInterval: 30_000,
|
||||||
|
refetchOnMount: 'always',
|
||||||
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: health } = useQuery({
|
const [upgrading, setUpgrading] = useState(false)
|
||||||
queryKey: ['system', 'health'],
|
const [upgradeElapsed, setUpgradeElapsed] = useState(0)
|
||||||
queryFn: async () => {
|
const [forceChecking, setForceChecking] = useState(false)
|
||||||
const r = await apiClient.get('/system/health')
|
const upgradePollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
return isEnvelope(r.data) ? (r.data.data as SystemHealth) : null
|
const upgradeTickRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
},
|
const installedRef = useRef<string>('')
|
||||||
// poll faster while we're applying so the version-flip is caught
|
const targetRef = useRef<string>('')
|
||||||
refetchInterval: applyingFor ? 2_000 : 30_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const upgrade = useMutation({
|
useEffect(() => () => {
|
||||||
mutationFn: async () => { await apiClient.post('/system/upgrade') },
|
if (upgradePollRef.current) clearInterval(upgradePollRef.current)
|
||||||
onError: (e: Error) => message.error(e.message),
|
if (upgradeTickRef.current) clearInterval(upgradeTickRef.current)
|
||||||
})
|
}, [])
|
||||||
|
|
||||||
// Detect version flip: api came back with the new version after
|
const data = pkgVersions.data ?? {}
|
||||||
// restart → upgrade succeeded, close modal + tell user.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!applyingFor || !health) return
|
|
||||||
if (health.version === applyingFor) {
|
|
||||||
message.success(t('update.success', { version: applyingFor }))
|
|
||||||
setApplyingFor(null)
|
|
||||||
setOpen(false)
|
|
||||||
// page reload picks up the rebuilt UI bundle
|
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
|
||||||
}
|
|
||||||
}, [applyingFor, health, t])
|
|
||||||
|
|
||||||
if (!data) return null
|
|
||||||
const updates = allUpdates(data)
|
const updates = allUpdates(data)
|
||||||
if (updates.length === 0) return null
|
const updateAvailable = updates.length > 0
|
||||||
|
|
||||||
// Anchor: prefer the meta package's available version as the
|
|
||||||
// "target" (since edgeguard depends on edgeguard-api/ui at the
|
|
||||||
// same version). Falls back to the first listed package.
|
|
||||||
const meta = updates.find(u => u.pkg === 'edgeguard') ?? updates[0]
|
const meta = updates.find(u => u.pkg === 'edgeguard') ?? updates[0]
|
||||||
|
const installedVersion = meta?.installed ?? ''
|
||||||
|
const targetVersion = meta?.available ?? ''
|
||||||
|
|
||||||
|
const forceCheck = async () => {
|
||||||
|
setForceChecking(true)
|
||||||
|
try {
|
||||||
|
await pkgVersions.refetch()
|
||||||
|
const fresh = pkgVersions.data ?? {}
|
||||||
|
const found = allUpdates(fresh).length > 0
|
||||||
|
msg[found ? 'success' : 'info'](
|
||||||
|
found ? t('update.checkDone') : t('update.noUpdate'),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
msg.error(t('update.checkFailed'))
|
||||||
|
} finally {
|
||||||
|
setForceChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startUpgrade = () => {
|
||||||
|
installedRef.current = installedVersion
|
||||||
|
targetRef.current = targetVersion
|
||||||
|
setUpgrading(true)
|
||||||
|
setUpgradeElapsed(0)
|
||||||
|
upgradeTickRef.current = setInterval(() => setUpgradeElapsed((e) => e + 1), 1000)
|
||||||
|
|
||||||
|
apiClient.post('/system/upgrade')
|
||||||
|
.then(() => {
|
||||||
|
// Poll /healthz (kein Auth, robust auch wenn die API gerade
|
||||||
|
// restartet und Cookie ihre Session nicht erkennt).
|
||||||
|
let sawDown = false
|
||||||
|
upgradePollRef.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get('/system/health')
|
||||||
|
const newV = (isEnvelope(res.data) ? (res.data.data as SystemHealth).version : '')
|
||||||
|
const flipped = newV && installedRef.current && newV !== installedRef.current
|
||||||
|
if (flipped || sawDown) {
|
||||||
|
if (upgradePollRef.current) clearInterval(upgradePollRef.current)
|
||||||
|
if (upgradeTickRef.current) clearInterval(upgradeTickRef.current)
|
||||||
|
setUpgrading(false)
|
||||||
|
msg.success(t('update.success', { version: targetRef.current }))
|
||||||
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Connection refused / 502 → API restartet. Beim nächsten
|
||||||
|
// erfolgreichen Poll erkennen wir den Version-Flip.
|
||||||
|
sawDown = true
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
// Sicherheits-Timeout: nach 2 Min einfach reload — falls der
|
||||||
|
// Restart länger braucht als erwartet, kommt die UI in jedem
|
||||||
|
// Fall wieder hoch.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (upgradePollRef.current) clearInterval(upgradePollRef.current)
|
||||||
|
if (upgradeTickRef.current) clearInterval(upgradeTickRef.current)
|
||||||
|
setUpgrading(false)
|
||||||
|
window.location.reload()
|
||||||
|
}, 120_000)
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
if (upgradeTickRef.current) clearInterval(upgradeTickRef.current)
|
||||||
|
if (upgradePollRef.current) clearInterval(upgradePollRef.current)
|
||||||
|
setUpgrading(false)
|
||||||
|
msg.error(t('update.failed') + ': ' + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateAvailable && !upgrading) {
|
||||||
|
return <>{msgCtx}</>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Alert
|
{msgCtx}
|
||||||
type="warning"
|
|
||||||
banner
|
|
||||||
showIcon
|
|
||||||
icon={<CloudDownloadOutlined />}
|
|
||||||
message={t('update.available', { pkg: meta.pkg, installed: meta.installed, available: meta.available })}
|
|
||||||
action={
|
|
||||||
<Button size="small" type="primary" onClick={() => setOpen(true)}>
|
|
||||||
{t('update.viewDetails')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
{updateAvailable && !upgrading && (
|
||||||
title={t('update.modalTitle')}
|
<Alert
|
||||||
open={open}
|
type="warning"
|
||||||
onCancel={() => { if (!applyingFor) setOpen(false) }}
|
banner
|
||||||
maskClosable={!applyingFor}
|
showIcon
|
||||||
closable={!applyingFor}
|
icon={<CloudDownloadOutlined />}
|
||||||
width={620}
|
message={t('update.available', { version: targetVersion })}
|
||||||
footer={
|
description={updates.length > 1
|
||||||
applyingFor ? null : [
|
? t('update.multiPackageHint', { count: updates.length })
|
||||||
<Button key="cancel" onClick={() => setOpen(false)}>{t('common.cancel')}</Button>,
|
: undefined}
|
||||||
<Button
|
action={
|
||||||
key="apply"
|
<Space>
|
||||||
type="primary"
|
<Button
|
||||||
icon={<CloudDownloadOutlined />}
|
size="small"
|
||||||
loading={upgrade.isPending}
|
icon={<ReloadOutlined />}
|
||||||
onClick={() => { setApplyingFor(meta.available); upgrade.mutate() }}
|
loading={forceChecking}
|
||||||
>
|
onClick={forceCheck}
|
||||||
{t('update.applyNow')}
|
>
|
||||||
</Button>,
|
{t('update.checkNow')}
|
||||||
]
|
</Button>
|
||||||
}
|
<Popconfirm
|
||||||
>
|
title={t('update.confirmTitle')}
|
||||||
{applyingFor ? (
|
description={t('update.confirmDesc', { version: targetVersion })}
|
||||||
<div>
|
okText={t('update.applyNow')}
|
||||||
<Alert
|
cancelText={t('common.cancel')}
|
||||||
type="info"
|
onConfirm={startUpgrade}
|
||||||
showIcon
|
>
|
||||||
message={t('update.applying')}
|
<Button size="small" type="primary" icon={<CloudDownloadOutlined />}>
|
||||||
description={t('update.applyingHint', { version: applyingFor })}
|
{t('update.applyNow')}
|
||||||
/>
|
</Button>
|
||||||
<div style={{ marginTop: 12, fontSize: 12, color: '#64748B' }}>
|
</Popconfirm>
|
||||||
{t('update.applyingDetail', { current: health?.version ?? '—', target: applyingFor })}
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{upgrading && (
|
||||||
|
<div className="update-modal-overlay">
|
||||||
|
<div className="update-modal">
|
||||||
|
<div className="update-modal__orbit">
|
||||||
|
<div className="update-modal__ring" />
|
||||||
|
<div className="update-modal__ring update-modal__ring--2" />
|
||||||
|
<div className="update-modal__dot" />
|
||||||
|
<div className="update-modal__dot update-modal__dot--2" />
|
||||||
|
<div className="update-modal__center">
|
||||||
|
<RocketOutlined className="update-modal__icon" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="update-modal__title">{t('update.running')}</div>
|
||||||
|
<div className="update-modal__version">
|
||||||
|
v{installedRef.current || '…'} → v{targetRef.current || '…'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="update-modal__steps">
|
||||||
|
<Step
|
||||||
|
done={upgradeElapsed >= 5}
|
||||||
|
active={upgradeElapsed < 5}
|
||||||
|
label={t('update.stepDownload')}
|
||||||
|
/>
|
||||||
|
<Step
|
||||||
|
done={upgradeElapsed >= 15}
|
||||||
|
active={upgradeElapsed >= 5 && upgradeElapsed < 15}
|
||||||
|
label={t('update.stepInstall')}
|
||||||
|
/>
|
||||||
|
<Step
|
||||||
|
done={upgradeElapsed >= 25}
|
||||||
|
active={upgradeElapsed >= 15 && upgradeElapsed < 25}
|
||||||
|
label={t('update.stepRestart')}
|
||||||
|
/>
|
||||||
|
<Step
|
||||||
|
done={false}
|
||||||
|
active={upgradeElapsed >= 25}
|
||||||
|
label={t('update.stepVerify')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="update-modal__timer">{upgradeElapsed}s</div>
|
||||||
|
<div className="update-modal__hint">{t('update.waitHint')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<>
|
)}
|
||||||
<Text>{t('update.modalIntro')}</Text>
|
|
||||||
<List
|
|
||||||
size="small"
|
|
||||||
style={{ marginTop: 12 }}
|
|
||||||
dataSource={updates}
|
|
||||||
renderItem={u => (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={<code>{u.pkg}</code>}
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
<Tag>{u.installed}</Tag> → <Tag color="blue">{u.available}</Tag>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
style={{ marginTop: 12 }}
|
|
||||||
message={t('update.modalWarn')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Step({ done, active, label }: { done: boolean; active: boolean; label: string }) {
|
||||||
|
const cls =
|
||||||
|
'update-modal__step' +
|
||||||
|
(active ? ' update-modal__step--active' : '') +
|
||||||
|
(done ? ' update-modal__step--done' : '')
|
||||||
|
return (
|
||||||
|
<div className={cls}>
|
||||||
|
<span className="update-modal__step-dot" />
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -306,17 +306,23 @@
|
|||||||
"setupCompleted": "Setup abgeschlossen"
|
"setupCompleted": "Setup abgeschlossen"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"available": "Update verfügbar: {{pkg}} {{installed}} → {{available}}",
|
"available": "Update verfügbar: Version {{version}}",
|
||||||
"viewDetails": "Details anzeigen",
|
"multiPackageHint": "{{count}} Pakete werden aktualisiert.",
|
||||||
"applyNow": "Jetzt aktualisieren",
|
"applyNow": "Jetzt installieren",
|
||||||
"applying": "Update läuft …",
|
"confirmTitle": "Update jetzt installieren?",
|
||||||
"applyingHint": "Pakete werden installiert + Services restartet. Verbindung kann kurz brechen — die UI lädt automatisch neu sobald Version {{version}} live ist.",
|
"confirmDesc": "Pakete werden auf Version {{version}} aktualisiert. edgeguard-api + scheduler restarten (~2-5s), HAProxy/nft/WG/Squid/Unbound/Chrony laufen durch.",
|
||||||
"applyingDetail": "aktuell {{current}} · Ziel {{target}}",
|
"checkNow": "Auf Updates prüfen",
|
||||||
"started": "Update wurde gestartet — der Server wird in Kürze neu starten.",
|
"checkDone": "Update verfügbar",
|
||||||
|
"noUpdate": "Keine neuen Updates",
|
||||||
|
"checkFailed": "Update-Check fehlgeschlagen",
|
||||||
|
"running": "Update läuft …",
|
||||||
|
"waitHint": "Bitte warten — die Seite lädt automatisch neu sobald die neue Version live ist.",
|
||||||
"success": "Update auf {{version}} abgeschlossen.",
|
"success": "Update auf {{version}} abgeschlossen.",
|
||||||
"modalTitle": "EdgeGuard-Update",
|
"failed": "Update fehlgeschlagen",
|
||||||
"modalIntro": "Folgende Pakete werden aktualisiert:",
|
"stepDownload": "Pakete laden",
|
||||||
"modalWarn": "Während des Updates werden edgeguard-api + edgeguard-scheduler neu gestartet (~2-5 Sekunden Service-Unterbrechung). HAProxy + nft + WireGuard + Squid + Unbound + Chrony bleiben durchgehend aktiv."
|
"stepInstall": "Installation",
|
||||||
|
"stepRestart": "Service-Restart",
|
||||||
|
"stepVerify": "Verifizierung"
|
||||||
},
|
},
|
||||||
"wg": {
|
"wg": {
|
||||||
"title": "WireGuard",
|
"title": "WireGuard",
|
||||||
|
|||||||
@@ -306,17 +306,23 @@
|
|||||||
"setupCompleted": "Setup completed"
|
"setupCompleted": "Setup completed"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"available": "Update available: {{pkg}} {{installed}} → {{available}}",
|
"available": "Update available: version {{version}}",
|
||||||
"viewDetails": "View details",
|
"multiPackageHint": "{{count}} packages will be updated.",
|
||||||
"applyNow": "Apply now",
|
"applyNow": "Install now",
|
||||||
"applying": "Update in progress …",
|
"confirmTitle": "Install update now?",
|
||||||
"applyingHint": "Packages are installed and services restarted. Connection may briefly break — the UI reloads automatically once version {{version}} is live.",
|
"confirmDesc": "Packages will be updated to version {{version}}. edgeguard-api + scheduler restart (~2-5 s), HAProxy/nft/WG/Squid/Unbound/Chrony stay running.",
|
||||||
"applyingDetail": "current {{current}} · target {{target}}",
|
"checkNow": "Check for updates",
|
||||||
"started": "Update has started — the server will restart shortly.",
|
"checkDone": "Update available",
|
||||||
|
"noUpdate": "No new updates",
|
||||||
|
"checkFailed": "Update check failed",
|
||||||
|
"running": "Update in progress …",
|
||||||
|
"waitHint": "Please wait — the page will reload automatically once the new version is live.",
|
||||||
"success": "Updated to {{version}}.",
|
"success": "Updated to {{version}}.",
|
||||||
"modalTitle": "EdgeGuard update",
|
"failed": "Update failed",
|
||||||
"modalIntro": "The following packages will be updated:",
|
"stepDownload": "Download packages",
|
||||||
"modalWarn": "During the update edgeguard-api + edgeguard-scheduler restart (~2-5 s service interruption). HAProxy + nft + WireGuard + Squid + Unbound + Chrony stay up the whole time."
|
"stepInstall": "Install",
|
||||||
|
"stepRestart": "Service restart",
|
||||||
|
"stepVerify": "Verification"
|
||||||
},
|
},
|
||||||
"wg": {
|
"wg": {
|
||||||
"title": "WireGuard",
|
"title": "WireGuard",
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart chrony.service
|
|||||||
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart chrony.service
|
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart chrony.service
|
||||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update -qq
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update -qq
|
||||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
||||||
|
# Self-Upgrade-Pfad (handlers/system.go → /system/upgrade). Whitelist
|
||||||
|
# nur die exakte Unit-Form, damit edgeguard NICHT beliebige systemd-
|
||||||
|
# Units anlegen darf.
|
||||||
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reset-failed edgeguard-upgrade.service
|
||||||
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemd-run --unit=edgeguard-upgrade.service --description=EdgeGuard self-upgrade --collect bash /tmp/edgeguard-upgrade.sh
|
||||||
SUDOERS
|
SUDOERS
|
||||||
|
|
||||||
# ── Distro-Conf-Includes für die per-Service Renderer ─────────
|
# ── Distro-Conf-Includes für die per-Service Renderer ─────────
|
||||||
|
|||||||
Reference in New Issue
Block a user