update 1.2.2.3 beta

This commit is contained in:
MacRimi
2026-07-06 00:30:47 +02:00
parent e07c83f4a9
commit 8df8a77bf1
5 changed files with 534 additions and 13 deletions

View File

@@ -1 +1 @@
7eb39d1bb3aed0750f32708fab66bf5c2c95a47da5722b9bd351b34c7c31b3ee ProxMenux-1.2.2.3-beta.AppImage
ab9f30de3517bf2aa0746691e230186e647fb68ad57a298837c7c871700e0c4c ProxMenux-1.2.2.3-beta.AppImage

View File

@@ -333,6 +333,190 @@ const formatRunAt = (iso: string | null) => {
}
}
// ── PBS keyfile management (shared between CreateJob + ManualBackup) ──
//
// Rendered inside the encryption section when a keyfile is already
// installed. Replaces the older static info box that pointed to a
// non-existent "Settings → Host backup" panel: the operator now has
// three concrete actions (change stored keyfile passphrase, replace
// keyfile, remove keyfile) with destructive-confirmation modals that
// call the /api/host-backups/pbs-encryption/* endpoints and mutate
// pbsRecoveryStatus so the parent dialog re-renders on success.
//
// `onReplaced` is called after a successful remove/replace-remove so
// the parent can reset its local pbsEncryptMode from "existing" back
// to "new" and let the Generate/Import radio unfold naturally.
function KeyfileManageInline({
reuseText,
onReplaced,
mutateStatus,
}: {
reuseText: string
onReplaced: () => void
mutateStatus: () => Promise<unknown>
}) {
const [action, setAction] = useState<null | "passphrase" | "replace" | "remove">(null)
const [busy, setBusy] = useState(false)
const [err, setErr] = useState<string | null>(null)
const [pass1, setPass1] = useState("")
const [pass2, setPass2] = useState("")
const [backupFirst, setBackupFirst] = useState(true)
const close = () => {
setAction(null)
setBusy(false)
setErr(null)
setPass1("")
setPass2("")
setBackupFirst(true)
}
const runPassphrase = async () => {
if (pass1 !== pass2) {
setErr("Passphrases do not match.")
return
}
setBusy(true)
setErr(null)
try {
await fetchApi("/api/host-backups/pbs-encryption/passphrase", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_passphrase: pass1 }),
})
await mutateStatus()
close()
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
setBusy(false)
}
}
const runRemove = async (mode: "remove" | "replace") => {
setBusy(true)
setErr(null)
try {
await fetchApi("/api/host-backups/pbs-encryption/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ backup_before_remove: backupFirst }),
})
await mutateStatus()
if (mode === "replace") onReplaced()
close()
} catch (e) {
setErr(e instanceof Error ? e.message : String(e))
setBusy(false)
}
}
return (
<div className="space-y-2">
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3 text-[11px] text-muted-foreground flex items-start gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
<div>{reuseText}</div>
</div>
<div className="flex flex-wrap gap-1.5">
<Button
type="button"
size="sm"
variant="outline"
className="h-7 text-[11px]"
onClick={() => setAction("passphrase")}
>
Update stored passphrase
</Button>
<Button
type="button"
size="sm"
variant="outline"
className="h-7 text-[11px]"
onClick={() => setAction("replace")}
>
Replace keyfile
</Button>
<Button
type="button"
size="sm"
variant="outline"
className="h-7 text-[11px] !text-red-400 border-red-500/40 hover:bg-red-500/10"
onClick={() => setAction("remove")}
>
Remove keyfile
</Button>
</div>
<Dialog open={action === "passphrase"} onOpenChange={(v) => !v && close()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Update stored keyfile passphrase</DialogTitle>
<DialogDescription>
This only rotates the passphrase ProxMenux uses to unlock the keyfile at backup time (stored in <code className="font-mono">/usr/local/share/proxmenux/pbs-key.pass</code>). It does <strong>not</strong> re-encrypt the keyfile itself.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<Label htmlFor="mpass1" className="text-xs">New passphrase</Label>
<Input id="mpass1" type="password" value={pass1} onChange={(e) => setPass1(e.target.value)} placeholder="Leave blank to remove the stored passphrase" className="font-mono h-9" />
</div>
{pass1 && (
<div>
<Label htmlFor="mpass2" className="text-xs">Confirm new passphrase</Label>
<Input id="mpass2" type="password" value={pass2} onChange={(e) => setPass2(e.target.value)} className="font-mono h-9" />
</div>
)}
{err && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={close}>Cancel</Button>
<Button onClick={runPassphrase} disabled={busy || (!!pass1 && pass1 !== pass2)}>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : "Save"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={action === "replace" || action === "remove"} onOpenChange={(v) => !v && close()}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-400" />
{action === "replace" ? "Replace keyfile" : "Remove keyfile"}
</DialogTitle>
<DialogDescription>
Backups already stored on PBS were encrypted with the current keyfile. After this action:
</DialogDescription>
</DialogHeader>
<ul className="text-xs text-muted-foreground list-disc list-inside space-y-1 pl-2">
<li>New backups will use {action === "replace" ? "the newly generated / imported keyfile" : "no encryption on this host until a new keyfile is set up"}.</li>
<li>Downloading pre-existing encrypted backups from this host <strong className="text-red-400">will fail</strong> unless you keep a copy of the current key.</li>
<li>Existing recovery blobs on PBS stay intact they still recover the old key with its original passphrase.</li>
</ul>
<label className="flex items-start gap-2 cursor-pointer text-xs">
<Checkbox checked={backupFirst} onCheckedChange={(v) => setBackupFirst(!!v)} className="mt-0.5" />
<span>Save a copy of the current keyfile + passphrase + recovery blob under <code className="font-mono">/root/pbs-key.old-&lt;timestamp&gt;.*</code> before {action === "replace" ? "replacing" : "removing"}.</span>
</label>
{err && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
)}
<DialogFooter>
<Button variant="outline" onClick={close} disabled={busy}>Cancel</Button>
<Button
onClick={() => runRemove(action === "replace" ? "replace" : "remove")}
disabled={busy}
className="!bg-red-500 hover:!bg-red-600 !text-white"
>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : (action === "replace" ? "Remove current — set up new" : "Remove")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
export function HostBackup() {
const { data: jobsResp, error: jobsErr, mutate: mutateJobs } = useSWR<{ jobs: BackupJob[] }>(
"/api/host-backups/jobs",
@@ -3058,12 +3242,11 @@ function CreateJobDialog({
</label>
{pbsEncrypt && pbsRecoveryStatus?.has_keyfile && (
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3 text-[11px] text-muted-foreground flex items-start gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
<div>
An encryption key is already installed on this host it will be reused for this job. To replace it, first import a new one from Settings Host backup or remove the existing keyfile.
</div>
</div>
<KeyfileManageInline
reuseText="An encryption key is already installed on this host — it will be reused for this job. Use the buttons below to change the stored passphrase, replace the key with a new one, or remove it entirely."
onReplaced={() => setPbsEncryptMode("new")}
mutateStatus={mutatePbsRecovery}
/>
)}
{pbsEncrypt && !pbsRecoveryStatus?.has_keyfile && (
@@ -3966,12 +4149,11 @@ function ManualBackupDialog({
</label>
{pbsEncrypt && pbsRecoveryStatus?.has_keyfile && (
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3 text-[11px] text-muted-foreground flex items-start gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
<div>
An encryption key is already installed on this host it will be reused for this backup.
</div>
</div>
<KeyfileManageInline
reuseText="An encryption key is already installed on this host — it will be reused for this backup. Use the buttons below to change the stored passphrase, replace the key with a new one, or remove it entirely."
onReplaced={() => setPbsEncryptMode("new")}
mutateStatus={mutatePbsRecovery}
/>
)}
{pbsEncrypt && !pbsRecoveryStatus?.has_keyfile && (

View File

@@ -13842,6 +13842,153 @@ def _pbs_recovery_decrypt(passphrase: str, in_path: str, out_path: str) -> bool:
return True
@app.route('/api/host-backups/pbs-encryption/keyfile-info', methods=['GET'])
@require_auth
def api_pbs_encryption_keyfile_info():
"""Snapshot of the installed keyfile for the "Manage keyfile" panel.
Extends /pbs-recovery/status with metadata that only makes sense
when a keyfile is actually installed (installed_at, kdf hint).
Returns 200 with `installed: false` when no keyfile is present so
the panel can render a neutral empty state without special-casing
404s in the caller."""
installed = os.path.isfile(_PBS_KEYFILE_PATH)
if not installed:
return jsonify({
'installed': False,
'path': _PBS_KEYFILE_PATH,
})
installed_at = ''
try:
installed_at = time.strftime(
'%Y-%m-%dT%H:%M:%SZ',
time.gmtime(os.path.getmtime(_PBS_KEYFILE_PATH)),
)
except OSError:
pass
# Best-effort KDF hint from the JSON keyfile body itself. `key info`
# would be the authoritative source but refuses scrypt keyfiles
# without their passphrase, so parsing the JSON manually gives us
# a hint even for imported scrypt keys the operator brought their
# own passphrase for.
kdf_hint = 'unknown'
try:
with open(_PBS_KEYFILE_PATH, 'r') as f:
raw = f.read(4096)
body = json.loads(raw)
kdf_raw = body.get('kdf')
if isinstance(kdf_raw, str):
kdf_hint = kdf_raw.lower()
elif isinstance(kdf_raw, dict):
# scrypt keyfiles carry `"kdf": {"Scrypt": {...}}`.
kdf_hint = next(iter(kdf_raw)).lower() if kdf_raw else 'unknown'
except (OSError, json.JSONDecodeError, ValueError, StopIteration):
pass
return jsonify({
'installed': True,
'path': _PBS_KEYFILE_PATH,
'fingerprint': _pbs_keyfile_fingerprint(),
'kdf_hint': kdf_hint,
'installed_at': installed_at,
'has_keyfile_passphrase': (
os.path.isfile(_PBS_KEYFILE_PASS_PATH)
and os.path.getsize(_PBS_KEYFILE_PASS_PATH) > 0
),
'has_recovery': os.path.isfile(_PBS_RECOVERY_ENC_PATH),
})
@app.route('/api/host-backups/pbs-encryption/remove', methods=['POST'])
@require_auth
def api_pbs_encryption_remove():
"""Remove the installed keyfile + its paired local artifacts
(passphrase file, recovery blob). Optionally backs the three up to
/root under a timestamped prefix so the operator can still decrypt
or rebuild them later. Never touches the recovery blob(s) already
uploaded to PBS — those stay recoverable with their original
passphrase and are the safe way to reach any pre-existing backup
encrypted with the removed key.
Body: `{backup_before_remove: bool}` (default true)
"""
payload = request.get_json(silent=True) or {}
do_backup = bool(payload.get('backup_before_remove', True))
if not os.path.isfile(_PBS_KEYFILE_PATH):
return jsonify({'error': 'no keyfile installed'}), 404
backed_up = []
if do_backup:
ts = time.strftime('%Y%m%d_%H%M%S', time.localtime())
for src, dst_suffix in (
(_PBS_KEYFILE_PATH, '.conf'),
(_PBS_KEYFILE_PASS_PATH, '.pass'),
(_PBS_RECOVERY_ENC_PATH, '.recovery.enc'),
):
if not os.path.isfile(src):
continue
dst = f'/root/pbs-key.old-{ts}{dst_suffix}'
try:
with open(src, 'rb') as fin, open(os.open(dst, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'wb') as fout:
fout.write(fin.read())
backed_up.append(dst)
except OSError as e:
return jsonify({
'error': f'backup to /root failed for {src}: {e}',
'partial_backups': backed_up,
}), 500
# Wipe order: recovery blob first so a mid-flight failure leaves us
# in a clean, backup-still-decryptable-with-blob state.
removed = []
for path in (_PBS_RECOVERY_ENC_PATH, _PBS_KEYFILE_PASS_PATH, _PBS_KEYFILE_PATH):
try:
os.remove(path)
removed.append(path)
except FileNotFoundError:
pass
except OSError as e:
return jsonify({
'error': f'remove failed for {path}: {e}',
'backed_up': backed_up,
'removed': removed,
}), 500
return jsonify({
'status': 'ok',
'backed_up': backed_up,
'removed': removed,
})
@app.route('/api/host-backups/pbs-encryption/passphrase', methods=['POST'])
@require_auth
def api_pbs_encryption_passphrase():
"""Update ONLY the stored keyfile passphrase (pbs-key.pass) without
touching the keyfile itself. Useful when the operator rotates the
passphrase on their own PBS setup and needs ProxMenux to catch up.
Body: `{new_passphrase}`. Empty value wipes the file — the runner
then treats the keyfile as kdf=none.
"""
payload = request.get_json(silent=True) or {}
new_pass = payload.get('new_passphrase') or ''
if not os.path.isfile(_PBS_KEYFILE_PATH):
return jsonify({'error': 'no keyfile installed'}), 404
if not new_pass:
try:
os.remove(_PBS_KEYFILE_PASS_PATH)
except FileNotFoundError:
pass
except OSError as e:
return jsonify({'error': f'cannot remove passphrase file: {e}'}), 500
return jsonify({'status': 'ok', 'has_keyfile_passphrase': False})
try:
fd = os.open(_PBS_KEYFILE_PASS_PATH,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'w') as f:
f.write(new_pass)
except OSError as e:
return jsonify({'error': f'cannot write passphrase file: {e}'}), 500
return jsonify({'status': 'ok', 'has_keyfile_passphrase': True})
@app.route('/api/host-backups/pbs-recovery/status', methods=['GET'])
@require_auth
def api_pbs_recovery_status():

View File

@@ -652,6 +652,7 @@ _bk_manage_destinations() {
1 "$(translate "Proxmox Backup Server (PBS) destinations")" \
2 "$(translate "Borg repositories")" \
3 "$(translate "Local archive targets (paths + USB mount/unmount)")" \
4 "$(translate "PBS encryption keyfile (show / replace / remove)")" \
0 "$(translate "Return")" \
3>&1 1>&2 2>&3) || break
@@ -666,11 +667,202 @@ _bk_manage_destinations() {
3)
_bk_manage_local_destinations
;;
4)
_bk_manage_pbs_encryption_keyfile
;;
0) break ;;
esac
done
}
# Backup the current keyfile + its paired files to /root under a
# timestamped prefix. Returns the timestamp on success (empty on
# failure). Never overwrites existing files. Used before any
# destructive change (replace / remove) so the operator can still
# decrypt pre-existing backups if they need to.
_bk_pbs_backup_keyfile_to_root() {
local ts key_file="$HB_STATE_DIR/pbs-key.conf"
local pass_file="$HB_STATE_DIR/pbs-key.pass"
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
[[ -f "$key_file" ]] || { echo ""; return 1; }
ts=$(date +%Y%m%d_%H%M%S)
umask 077
cp -f "$key_file" "/root/pbs-key.old-${ts}.conf" 2>/dev/null || { echo ""; return 1; }
chmod 600 "/root/pbs-key.old-${ts}.conf" 2>/dev/null || true
if [[ -f "$pass_file" ]]; then
cp -f "$pass_file" "/root/pbs-key.old-${ts}.pass" 2>/dev/null || true
chmod 600 "/root/pbs-key.old-${ts}.pass" 2>/dev/null || true
fi
if [[ -f "$recovery_enc" ]]; then
cp -f "$recovery_enc" "/root/pbs-key.old-${ts}.recovery.enc" 2>/dev/null || true
chmod 600 "/root/pbs-key.old-${ts}.recovery.enc" 2>/dev/null || true
fi
echo "$ts"
}
# Wipe local keyfile + pass + recovery. Never touches PBS.
_bk_pbs_wipe_local_keyfile() {
rm -f "$HB_STATE_DIR/pbs-key.conf" \
"$HB_STATE_DIR/pbs-key.pass" \
"$HB_STATE_DIR/pbs-key.recovery.enc" 2>/dev/null
}
# Dedicated management submenu for the PBS encryption keyfile.
# Available as option 4 of "Configure backup destinations". All
# destructive actions offer a backup-to-/root step first and require
# an explicit confirmation before touching disk.
_bk_manage_pbs_encryption_keyfile() {
local key_file="$HB_STATE_DIR/pbs-key.conf"
local pass_file="$HB_STATE_DIR/pbs-key.pass"
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
while true; do
local installed=0
[[ -s "$key_file" ]] && installed=1
local preview=""
if (( installed )); then
local fp="" kdf_hint="unknown" installed_at=""
if command -v proxmox-backup-client >/dev/null 2>&1; then
fp=$(proxmox-backup-client key info --output-format json "$key_file" 2>/dev/null \
| grep -oE '"fingerprint"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 | sed 's/.*"\([^"]*\)".*/\1/')
fi
if grep -q '"kdf":[[:space:]]*"None"' "$key_file" 2>/dev/null; then
kdf_hint="none"
elif grep -q '"Scrypt"' "$key_file" 2>/dev/null; then
kdf_hint="scrypt"
fi
installed_at=$(date -r "$key_file" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "")
preview="$(hb_translate "Installed:") \Zb\Z2$(hb_translate "yes")\Zn"$'\n'
preview+="$(hb_translate "Path:") $key_file"$'\n'
[[ -n "$installed_at" ]] && preview+="$(hb_translate "Installed at:") $installed_at"$'\n'
preview+="$(hb_translate "KDF:") $kdf_hint"$'\n'
[[ -n "$fp" ]] && preview+="$(hb_translate "Fingerprint:") $fp"$'\n'
preview+="$(hb_translate "Keyfile passphrase stored:") "
[[ -s "$pass_file" ]] && preview+="\Zb\Z2$(hb_translate "yes")\Zn"$'\n' || preview+="\Zb$(hb_translate "no")\Zn"$'\n'
preview+="$(hb_translate "Recovery escrow present:") "
[[ -s "$recovery_enc" ]] && preview+="\Zb\Z2$(hb_translate "yes")\Zn"$'\n' || preview+="\Zb$(hb_translate "no")\Zn"$'\n'
else
preview="$(hb_translate "No keyfile installed on this host.")"$'\n\n'
preview+="$(hb_translate "PBS encrypted backups won't work until one is generated or imported. Both actions happen during job creation the first time you tick 'Encrypt backups'.")"
fi
local choice
if (( installed )); then
choice=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate "Manage PBS encryption keyfile")" \
--menu "\n${preview}\n" \
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
"info" "$(hb_translate "Show detailed keyfile info")" \
"pass" "$(hb_translate "Update stored keyfile passphrase")" \
"replace" "$(hb_translate "Replace keyfile (generate new or import)")" \
"remove" "$(hb_translate "Remove keyfile from this host")" \
"back" "$(hb_translate "← Return")" \
3>&1 1>&2 2>&3) || break
else
choice=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate "Manage PBS encryption keyfile")" \
--menu "\n${preview}\n" \
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
"back" "$(hb_translate "← Return")" \
3>&1 1>&2 2>&3) || break
fi
case "$choice" in
info)
# Full `key info` output (may need passphrase for scrypt).
local info_out
info_out=$(proxmox-backup-client key info "$key_file" 2>&1)
dialog --backtitle "ProxMenux" \
--title "$(hb_translate "Keyfile info")" \
--msgbox "$info_out" 20 80
;;
pass)
local new_pass new_pass2
while true; do
new_pass=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Update keyfile passphrase")" \
--insecure --passwordbox "$(hb_translate "New passphrase used to unlock the keyfile (leave blank to remove the stored passphrase — for kdf=none keyfiles):")" \
"$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || { new_pass=""; break; }
if [[ -z "$new_pass" ]]; then
new_pass2=""
break
fi
new_pass2=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Update keyfile passphrase")" \
--insecure --passwordbox "$(hb_translate "Confirm new passphrase:")" \
"$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || { new_pass=""; break; }
[[ "$new_pass" == "$new_pass2" ]] && break
dialog --backtitle "ProxMenux" \
--msgbox "$(hb_translate "Passphrases do not match. Try again.")" 8 50
done
if [[ -n "$new_pass" ]]; then
umask 077
printf '%s' "$new_pass" > "$pass_file"
chmod 600 "$pass_file" 2>/dev/null || true
dialog --backtitle "ProxMenux" \
--msgbox "$(hb_translate "Stored keyfile passphrase updated.")" 8 60
else
rm -f "$pass_file" 2>/dev/null
dialog --backtitle "ProxMenux" \
--msgbox "$(hb_translate "Stored keyfile passphrase removed.")" 8 60
fi
;;
replace|remove)
local action_title="$(hb_translate "Replace keyfile")"
[[ "$choice" == "remove" ]] && action_title="$(hb_translate "Remove keyfile")"
# Destructive warning + confirmation
local warn_body
warn_body="\Zb\Z1$(hb_translate "This is a destructive action")\Zn"$'\n\n'
warn_body+="$(hb_translate "Backups already stored on PBS were encrypted with the current keyfile. After this action:")"$'\n'
warn_body+="$(hb_translate "New backups will use the new (or no) keyfile.")"$'\n'
warn_body+="$(hb_translate "Downloading pre-existing encrypted backups from this host will fail unless you keep a copy of the current key.")"$'\n'
warn_body+="$(hb_translate "Existing recovery blobs on PBS stay intact — they still recover the old key with its original passphrase.")"$'\n\n'
warn_body+="$(hb_translate "Continue?")"
if ! dialog --backtitle "ProxMenux" --colors --yesno "$warn_body" 16 80; then
continue
fi
local do_backup=1
if ! dialog --backtitle "ProxMenux" \
--title "$(hb_translate "Backup current key")" \
--yesno "$(hb_translate "Save a copy of the current keyfile + passphrase + recovery blob under /root/pbs-key.old-<timestamp>.* before ${action_title,,}?")" \
10 78; then
do_backup=0
fi
local ts=""
if (( do_backup )); then
ts=$(_bk_pbs_backup_keyfile_to_root)
if [[ -z "$ts" ]]; then
dialog --backtitle "ProxMenux" \
--msgbox "$(hb_translate "Backup to /root failed. Aborting.")" 8 60
continue
fi
fi
_bk_pbs_wipe_local_keyfile
if [[ "$choice" == "replace" ]]; then
# Delegate to the yes/no + generate/import flow.
# HB_ASK_ENCRYPT_INSTRUCTIONS is unused here, we
# directly call the two helpers.
local rmenu
rmenu=$(dialog --backtitle "ProxMenux" \
--title "$(hb_translate "Replace keyfile")" \
--menu "$(hb_translate "How should the new keyfile be set up?")" \
10 78 2 \
"new" "$(hb_translate "Generate a new keyfile (per host — safest isolation)")" \
"import" "$(hb_translate "Import an existing keyfile from a path (shared across hosts)")" \
3>&1 1>&2 2>&3) || rmenu=""
case "$rmenu" in
new) _hb_pbs_create_new_keyfile || true ;;
import) _hb_pbs_import_dialog || true ;;
esac
fi
local done_msg="$action_title $(hb_translate "completed.")"
[[ -n "$ts" ]] && done_msg+=$'\n\n'"$(hb_translate "Backup saved with prefix:") /root/pbs-key.old-${ts}"
dialog --backtitle "ProxMenux" --msgbox "$done_msg" 10 78
;;
back|"") break ;;
esac
done
}
_bk_manage_extra_paths() {
while true; do
local -a paths=()