Three bugs against the PBS encryption flow:
1. Create-scheduled-job with encryption failed with "Recovery setup
failed: no PBS keyfile present" whenever the operator picked
"Generate a new keyfile" but had no keyfile installed yet. The
frontend called /pbs-recovery/setup before creating the job, but
the keyfile was only materialised later during job creation. The
endpoint now generates the keyfile atomically if missing before
building the escrow blob — same prompt-first order the CLI wizard
applies. Existing keyfiles are still trusted and never rotated.
2. Importing a valid PBS keyfile via the Web dialog returned a
generic "did not recognise this file as a valid PBS keyfile" that
hid the real reason (kdf mismatch, missing passphrase, corrupt
JSON, ...). The endpoint now attaches the stderr of
`proxmox-backup-client key info` as `tool_output` and the frontend
renders it verbatim inside the red banner. Also strips a leading
UTF-8 BOM before validating so an editor-inserted BOM stops being
silently classified as "invalid keyfile".
3. Downloading an encrypted PBS snapshot failed with "missing key —
manifest was created with key XX:XX:..." even when the correct
keyfile was installed at /usr/local/share/proxmenux/pbs-key.conf,
because the restore worker invoked `proxmox-backup-client restore`
without `--keyfile`. The flag is now passed whenever a local
keyfile exists (PBS ignores it for unencrypted archives). On a
fingerprint mismatch the error now appends the installed key's
fingerprint so it can be compared side-by-side with the manifest's
expected value — same fingerprint also exposed via
/pbs-recovery/status for the UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A mass-backup webhook that exceeded ~2 KB used to be silently
truncated by `desc = message[:MAX_EMBED_DESC]` with MAX_EMBED_DESC
set to 2048 — half of Discord's real description limit and far
below what a multi-VM backup digest produces. The trailing jobs
just vanished from the channel.
Bring the channel up to Discord's actual webhook contract:
* description limit raised to the real 4096-char cap
* if the body still doesn't fit, split it on line boundaries into
one embed per chunk so every backup entry is preserved
* keep title + fields on the first embed only; attach the footer
and timestamp to the last embed so the rendered card has the
normal head/tail framing even when split across many embeds
* enforce Discord's 6000-char-per-embed cap (title + description +
every field name+value) — only kicks in when many large fields
combine with a chunk already near the description ceiling
* batch up to 10 embeds per webhook POST (Discord's per-message
limit) and POST additional messages sequentially with a 0.4 s
gap so a >10-embed digest doesn't trip the 5/2 s webhook rate
limit
Verified with synthetic mass-backup payloads:
* 14 KB / 200 jobs → 4 embeds, 1 POST
* 60 KB / 60 lines → 15 embeds, 2 POSTs (10 + 5)
New AppImage SHA-256:
16ad59ea63a64e5be460cd73f87315e8b39b756bf1c61f3cb2019e9fa3e76361
Closes#220.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`get_disks_observation_counts` maps each serial's count to that
serial's "most recent" device_name (so renames like ata8 -> sdh keep
the badge attached). When several physical disks have passed through
the same kernel name across reboots — common with NVMe, the kernel
probes in a different order depending on which slots are populated —
disk_registry keeps a row per (device_name, serial) seen and the
"most recent" device_name for a serial can now be in use by an
entirely different disk.
Concrete case from the wild: serial 211716800490 was nvme0n1 during
the previous boot and earned a real I/O observation. After removing
four of five NVMes, the surviving disk (serial 243332800236) booted
into nvme0n1. The badge layer mirrored 211716800490's count onto
nvme0n1 — which is now a different physical disk — and showed
"1 obs." on the wrong drive, while the modal (which scopes by the
current (device_name, serial) registry row) found nothing and
rendered an empty history.
Only mirror a serial's count onto its device_name when that
device_name is currently owned by the same serial, determined from
the freshest disk_registry row. The serial-keyed entry stays
unconditional so observations remain reachable when the disk is
re-plugged under another device name.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The UPDATE in `_resolve_error_impl` only touched `resolved_at` — the
`reason` argument every caller passes was silently dropped, and the
`resolution_reason` / `resolution_type` columns stayed NULL for every
auto-resolved error. The columns were added back in a previous sprint
for exactly this audit-log purpose, but the writer was never updated
to populate them.
Fix the SQL to write `resolution_reason = ?` and tag
`resolution_type = COALESCE(existing, 'auto')` so admin-cleared
errors (whose type is set elsewhere) keep their value while the
default auto path correctly labels itself.
Verified end-to-end on the lab host: re-injected the `disk_nvme2n1`
warning, waited one scan cycle, the row now reads
`resolution_type='auto'` and
`resolution_reason='Transient I/O cleared, SMART now reports healthy'`
— previously these columns stayed NULL even though the resolve_error
call passed a descriptive reason.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a host gets transient I/O events on a disk while smartctl is
momentarily unavailable (the canonical case: late in a noisy
shutdown), the disk-scan code records a `disk_<name>` WARNING tagged
"SMART: unavailable" exactly once and trusts the next scan to clear
it. That trust is misplaced: the clear path only fires when the
device shows up in the current dmesg window with zero events. After
a reboot, dmesg is empty for that device — so the device never gets
iterated, resolve_error is never called, and the dashboard stays
orange for a disk whose SMART now reports PASSED.
Caught on a lab host where `disk_nvme2n1` had been stuck as WARNING
for hours after a reboot. SMART was 100% healthy at the moment of
inspection (Critical Warning 0x00, 0 media errors, 100% spare). The
error's first_seen and last_seen were identical and pre-dated the
current boot, confirming a one-shot record that nothing had cleared.
Fix: add a `_reconcile_stale_disk_warnings()` pass at the top of
`_check_disks_optimized()`. For every active `disk_*` error
(skipping `disk_fs_*`, which is already reconciled separately):
- device gone from /dev/ → resolve "Device no longer present"
- device present + SMART PASSED → resolve "Transient I/O cleared,
SMART now reports healthy"
- device present + SMART UNKNOWN/FAILED → leave active so the
main loop can re-classify on the next dmesg window
Acknowledged errors are left alone so the user's explicit dismiss
intent isn't overridden.
Verified end-to-end: re-injected the original `disk_nvme2n1`
warning into the persistence DB on the lab host, waited one scan
cycle, error was resolved automatically with `resolved_at` set and
`resolution_reason = 'Transient I/O cleared, SMART now reports
healthy'`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>