# Changelog All notable changes to the Vault plugin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project uses date-based versioning (`YYYY.MM.PATCH`). ## [Unreleased] ### Added - **VM backup jobs now detect raw vs qcow2 disks and only offer supported backup types** (closes #163). Vault reads each VM's disk format at discovery time and exposes it (`disk_format`, `supports_incremental`) on `GET /api/v1/vms`. Because libvirt checkpoint-based incremental/differential backups require qcow2 disks, the job wizard now hides those options — and falls back to Full — when a selected VM uses raw or mixed disks, with an inline note naming the affected VM. This surfaces up front the constraint the engine previously enforced silently by downgrading raw-disk VMs to full backups. - **Pre/post-backup scripts now receive job context as environment variables** (part of #187). The job editor has long promised `VAULT_JOB_NAME` and `VAULT_STATUS` to hook scripts, but they were never actually set. Vault now exports `VAULT_JOB_NAME`, `VAULT_STATUS` (`starting` for pre-scripts, the run's final status for post-scripts), `VAULT_JOB_ID`, and `VAULT_RUN_ID` into every pre/post-backup script's environment — enabling, for example, an Immich `pg_dump` pre-script that targets the right job. - **Container exclusion presets can now carry advisory notes and warnings** (part of #187). `GET /api/v1/presets/exclusions` returns optional `notes`/`warnings` for apps that need a caveat, and the job wizard shows them inline beneath the container's exclusions. - **Docker named volumes are now backed up, not silently skipped.** Vault previously only backed up bind mounts, so containers that store their data in named `docker volume`s (many compose stacks — the single most-reported gap for the old appdata.backup plugin) were archived empty. Vault now backs up named volume mounts too — a volume's data lives at a real host path (`/var/lib/docker/volumes//_data`), handled exactly like a bind mount — across the classic, dedup and restore paths, and reattaches them on restore via the container's bind spec. The job wizard's mount editor lists them tagged "named volume" so you can include or exclude each one. Anonymous volumes (a random 64-hex id) are deliberately skipped because they can't be reliably reattached on restore; tmpfs/npipe and other non-data mounts are skipped as before. - **Compression level is now configurable per job** (fastest / default / better / best). The job wizard exposes a level next to the compression algorithm, mapped to the zstd/gzip encoder levels, so you can trade backup time for archive size. The default level is unchanged, so existing jobs and their archives are byte-for-byte identical. - **Jobs that would back up into their own source are now rejected up front.** Saving a job whose local destination path sits inside (or contains) one of its folder/flash source trees now returns a clear error instead of silently archiving each run into the next run's source — the old plugin's "backing itself up" footgun. Remote destinations are unaffected. - **Sparse files that could stall a backup are now flagged.** Vault logs a warning when a large file's logical size vastly exceeds its on-disk size (e.g. a Meilisearch/LMDB store reporting terabytes but occupying kilobytes), so a slow backup reading the full logical extent is no longer a silent mystery — the exact trap behind the old plugin's multi-hour stalls. - **Containers that route their network through another container are flagged.** When a container uses `network_mode: container:` (a Gluetun/VPN-sidecar setup), Vault now logs a note reminding you to include the provider in the same job and order it to start first, so the dependent doesn't lose its network when restarted after backup. ### Fixed - **Backups no longer fail when the Recycle Bin plugin is installed** (closes #204). The Unraid Recycle Bin plugin creates a `/mnt/RecycleBin` view directory (symlinks/subfolders, not a real pool) that it rebuilds periodically. Vault's staging-pool discovery treated it as a normal pool and staged backups there, so a docker or flash backup could fail mid-run with `open /mnt/RecycleBin/.vault-stage/…: no such file or directory` when the plugin regenerated the view. `/mnt/RecycleBin` (and any dot-prefixed `/mnt` entry such as a stray `.Recycle.Bin`) is now excluded from staging-pool candidates. - **Flash and folder backups now honour path exclusions** (closes #204). The classic tar backup path used for flash/folder items on non-dedup destinations ignored `exclude_paths` entirely, and the job wizard offered no way to set them. Folder items (including the Flash Drive preset) now have an "Exclude paths" editor in the wizard's advanced step, and the engine applies those exclusions on the classic path just as it already did for deduplicated backups — so you can, for example, exclude a share's `.Recycle.Bin` folder from a flash backup. - **The Recycle Bin plugin's trash folder is auto-excluded from the Flash Drive backup when detected** (closes #204). Vault now detects the installed Recycle Bin plugin (via its `/boot/config/plugins/recycle.bin.plg` marker) and, when present, pre-fills `.Recycle.Bin` as a recommended exclusion for the Flash Drive backup in the job wizard — so deleted files held in the trash aren't backed up. The pre-filled exclusion is shown in the editor and can be changed or removed. ### Changed - **Rate limiting on the encryption-verify and API-key endpoints now uses an explicit client-IP key.** Migrated off `httprate`'s `LimitByIP`, which a recent dependency bump deprecated (tripping the lint/build gate), to `LimitBy` with a documented key function. Behaviour is unchanged — requests are still bucketed by the connecting TCP peer address — the trust model is just stated explicitly now. - **Anomaly messages are now plain-English sentences** (closes #192). Every detector now describes what happened versus what was expected in friendly language — e.g. _"This backup took 4m 23s, about 1.2× its usual 3m 40s"_ instead of _"backup duration anomaly: 4m 23s (1.2x median)"_ — across the dashboard, the anomalies page, notifications, the API, and MCP. Notifications also replace the raw JSON blob (`{"z_score":11.06,...}`) with a short context line such as _"Based on the last 10 samples"_, while the precise statistics remain available in the structured `details` field for programmatic consumers. - **Tdarr container backups now drop stray log files and are covered by tests** (closes #188). The Tdarr exclusion preset already skipped the transcode cache (`/temp`) and log directory (`/app/logs`); it now also excludes stray `*.log` files, while deliberately keeping `/app/server` (the DB2 database, plugins and samples) and `/app/configs` so Tdarr settings and statistics restore cleanly. Both the combined `tdarr` image and the `tdarr_node` image are recognised, now with regression tests to keep detection working. - **Immich container backups are now correct for both image variants** (closes #187). The exclusion preset previously only covered the imagegenius fork's `/photos` layout; it now also covers the official `immich-app` image's `/data` layout, excluding only regeneratable thumbnails and re-encoded video while always keeping the original `upload`/`library`/`profile` assets and Immich's built-in `backups/` database dumps. The wizard now also surfaces a warning that Immich's PostgreSQL database runs in a separate container and must be captured via Immich's built-in database backup, a dedicated Postgres backup job, or a `pg_dump` pre-script — see the Backup Jobs guide in the documentation for the recommended setup. ## [2026.07.01] - 2026-07-04 ### Added - **Documentation link in Settings → Reference** (closes #189). A new Documentation card at the top of the Reference tab links directly to the online manual at [ruaan-deysel.github.io/vault](https://ruaan-deysel.github.io/vault/), so users can reach the guides and settings reference without leaving the plugin. - **Optional "Backups" quick link in the Unraid top navigation** (closes #162). Enable it under Settings → Utilities → Vault → Quick Access to get a one-click **Backups** entry in the Unraid top bar that opens the Vault web UI embedded in the Unraid interface. Off by default. Unlike the community add-on that inspired it, this uses Unraid's supported plugin menu extension point — no core files are patched and no background watchdog is needed; the embedded view also refreshes its session token automatically when you return to a long-idle tab. ### Fixed - **Anomaly baselines no longer report a 100% failure rate for every job** (closes #181). The baseline refresher counted a run as failed unless its status was `success` — a value the runner never writes (successes are `completed`) — so every job's stored failure rate was a meaningless constant 1.0. Failures are now counted the same way the reliability detector counts them (`failed` status or failed items), producing real rates. - **Database snapshots are now written atomically** (closes #182). The pool snapshot and USB safety-net copy were written directly over the previous good file, so a crash, power loss, or full disk mid-save corrupted the very copy the daemon needs for disaster recovery. Snapshots are now written to a temp file and renamed into place only after completing successfully — and if the primary snapshot is ever unreadable at startup, the daemon now also tries the rotated snapshot copies (newest first) before falling back to the USB copy or a fresh database. - **Post-backup health checks show container names again after a container was recreated** (closes #178). In stop-all mode, the health check after restarting containers looked names up by the stored container ID — but when a container had been recreated (update, compose recreate) the runner works with the re-resolved ID, so the check reported the raw 64-character ID instead of the name. Names now resolve through the re-resolved IDs too. - **Filename validation now rejects single backslashes** (closes #179). The path-separator guard in restore-name validation checked for a double backslash due to an escaping mistake, letting a single `\` through this specific check (harmless on Linux, but the guard didn't do what it said). It now rejects both `/` and `\`. - **Replication sync results are now honest about failures** (closes #177). A sync whose jobs all failed to copy still recorded "success" and announced a completed sync — a false safety signal for an off-site-copy feature. Sync status is now `success` only when every job synced, `partial` when some failed, and `failed` when nothing synced, with the failure count shown on the Replication page (which previously showed failed syncs as "Pending"). - **Replicated restore-point metadata can no longer be silently corrupted** (closes #176). The replication origin marker was spliced into restore-point metadata by string concatenation, so a source name containing quotes/backslashes — or metadata like `{ }` — produced invalid JSON, silently stripping checksums and dedup manifest references from every replicated restore point. The metadata is now rebuilt through the JSON encoder and is always valid. - **Restoring over an existing installation no longer fails on pre-existing symlinks** (closes #175). Regular files are overwritten during restore, but symlink and hardlink entries were created without replacing what was already on disk — so restoring a container/folder backup onto a live appdata tree aborted with "file exists" at the first pre-existing link (version symlinks, certificate links, node_modules/.bin). Links are now replaced like files; directories are never silently removed. - **Deduplicated backups no longer fail outright on a single unreadable file** (closes #174). Classic backups skip inaccessible files with a log line, but the deduplicated (chunked) path aborted the entire item on the first permission-denied or vanished file — so the same job succeeded on a classic destination and hard-failed on a dedup one. The chunked walk now skips-and-logs identically, reporting the number of skipped paths in the log and progress message. - **Automatic database backups are now pruned instead of accumulating forever** (closes #184). After every successful run Vault copies its own database to the destination's `_vault/` directory, but old copies were never removed — one server had accumulated 136 copies (477 MB) in six weeks, and on quota-limited remotes this growth eventually crowds out real backups. Vault now keeps the newest 14 timestamped copies per destination (plus the `vault.db.latest` recovery pointer, which is always kept) and deletes older ones after each write. - **Deleting the last dedup job on a destination no longer destroys the daemon's database backups stored there** (closes #183). The dedup chunk repository and the automatic `vault.db` recovery copies share the `_vault` directory on every destination; the cleanup that removes an orphaned dedup repo deleted the entire directory — including every database backup. Cleanup now removes only the dedup-owned paths (`repo.json`, `packs/`, `index/`) and leaves the database copies untouched. - **Replication syncs no longer leak storage connections** (closes #173). Each replication sync opened the local storage destination but never closed it; for SFTP destinations that leaked up to four SSH connections per scheduled sync (and orphaned NFS mounts), accumulating until daemon restart. The adapter is now released when the sync completes. - **Daemon shutdown now waits for in-flight restores** (closes #172). A planned restart (plugin update, `rc.vault restart`) drained active backups but was blind to restores: shutdown could kill a restore mid-write, leaving half-restored appdata or VM disks. Restores now participate in the same drain protocol as backups — shutdown waits for them, and a draining daemon refuses to start new ones. - **Cancelling a VM backup or restore now takes effect immediately** (closes #171). VM operations ignored cancellation entirely: pressing Cancel (or the stall watchdog firing) had no effect until the libvirt backup job finished or its 2-hour timeout expired, and the hypervisor kept writing disk data into a staging directory the runner had already torn down. Cancellation is now honoured within seconds across every VM stage — the in-flight libvirt backup job is aborted (`DomainAbortJob`), multi-GB disk/NVRAM copies stop at the next 1 MiB chunk, and all shutdown/startup/readiness waits exit early — while VM teardown (restarting a cold-backed-up VM, tearing down the paused backup session) still always completes so a cancelled run never leaves a VM in the wrong power state. - **Backups of imported jobs no longer crash when a container's stored ID is missing** (closes #170). Jobs imported from existing backups (native manifests without per-item IDs, or appdata.backup imports) store containers with an empty ID; when such a job ran while a container with that name existed, the ID-change log line sliced the empty ID and panicked, failing the run with an opaque "Internal error (panic)" on every attempt. ID truncation is now bounds-checked everywhere. - **SMB capacity probes no longer leak a connection on every check** (closes #169). The scheduled capacity probe (and the Storage page's refresh button) opened an SMB session for the space query but never logged it off, leaking one authenticated session plus its TCP connection per probe until the NAS started refusing new SMB connections. The probe now releases the session like every other SMB operation. - **Empty directories left behind by deleted backups are now actually cleaned up** (closes #168). Deleting restore points (retention, manual deletion) removed the files but the sweep that prunes the now-empty parent directories never ran: the storage middleware chain didn't pass the directory-removal capability through to the underlying adapter, so the sweep silently did nothing on every destination. Local, SFTP, SMB, and NFS destinations now prune emptied parent directories again; object stores (S3) and WebDAV, which have no safe "remove if empty" primitive, are skipped cleanly. - **ZFS incremental and differential chains now record and use the correct parent snapshot** (closes #180). The snapshot created by each ZFS backup was never persisted into the restore point, so chained runs silently fell back to sending against whatever vault snapshot happened to be newest on the dataset — a "differential" was really an incremental against the previous run, and snapshot cleanup then destroyed the last full's snapshot (the chain anchor). The result: differential restore chains could not be received (`zfs recv: most recent snapshot does not match incremental source`). Each backup now records its snapshot per item in the restore point (`zfs_snapshots`), the next chain run resolves its `-i` parent from there (falling back to a full backup with a clear warning when the parent is missing or was deleted — including for restore points created before this fix), and cleanup preserves the chain-anchor snapshot alongside the newest one. - **Automatic retries now actually fire for pre-flight failures and stall-cancelled runs** (closes #167). When a run was skipped because its destination failed the pre-flight check (DNS blip, unmounted share), or cancelled by the no-progress stall watchdog, Vault scheduled a retry — but the retry watcher only ever dispatched retries for runs whose status was `failed`, so those scheduled retries sat in the database forever and never ran. The watcher now claims due retries for `failed`, `skipped`, and `cancelled` runs alike, matching the outcomes retries are scheduled for. - **Truncated archives can no longer be reported as successful backups** (closes #166). When finalising a container-volume, folder, or plugin archive, errors from the tar footer flush, the gzip/zstd trailer, and the final write to disk were silently discarded — so a disk that filled up during the last flush, or a file that shrank while being archived, produced a corrupt/truncated archive that the run reported (and even checksum-verified) as success, discovered only at restore time. All three finalisation steps now fail the backup with a clear error (`closing tar writer: …`, `finalising compression: …`, `closing archive file: …`). - **Deduplication garbage collection can no longer run while a backup is writing to the same destination** (closes #165). Starting a GC (Storage → Run GC, or the automatic reclaim after deleting a dedup job) while a dedup backup was in flight could delete the chunk packs that backup had just written — its restore point only becomes visible to GC at the very end of the run — silently corrupting the new backup so it would fail at restore time. GC now waits for the active backup/restore to finish before it scans (and backups queue behind a running GC), refuses to start during daemon shutdown, and shutdown in turn waits for an in-progress GC to complete. - **S3 backup verification no longer aborts (and deletes the upload) on large archives** (closes #164). S3 `Read`/`ReadRange` bound the streamed response body to the same 5-minute deadline used for quick metadata calls, so verifying any archive that takes longer than 5 minutes to stream back failed with `context deadline exceeded` — and the runner then removed the files it had just successfully uploaded, leaving the run failed and the destination empty. Streamed S3 reads now carry no wall-clock deadline (matching SFTP/WebDAV/local): closing the reader releases the connection, waiting for a dead endpoint's response headers is still bounded (5 minutes), and genuinely hung transfers are cancelled by the stall watchdog. Deep verify and restores from S3 destinations benefit the same way. ## [2026.07.00] - 2026-07-02 ### Fixed - **VM backups now honour the job's compression setting.** Container, folder, and plugin backups compress their archives in the engine, but VM backups stage raw artifacts (disk images, `domain.xml`, `vm_meta.json`, NVRAM) and the upload layer never applied a codec — so jobs configured for gzip or Zstandard still landed full-size, uncompressed `qcow2`/`img` files on the destination. VM uploads are now compressed in transit with the configured codec (stored as `.gz`/`.zst`, compressed before encryption) and transparently decompressed on restore; existing uncompressed backups keep restoring unchanged. A 10 GiB Fedora test disk dropped from 2.7 GB to 1.1 GB on the destination with zstd. - **VM backups no longer fail with `backup job ended unexpectedly: DomainJobNone`** (closes #160). When a libvirt push-backup job finished, Vault asked libvirt for the completed-job record to decide success or failure. On hosts where that record is unavailable at poll time (libvirt reports "no completed job" as a normal reply, not an error — e.g. another tool consumed the record or libvirtd reloaded), Vault misread the reply as a failure even though every byte had been copied, so backups that ran to completion (44+ minutes of real work) were reported as failed on every attempt. Vault now treats a missing completion record as "no verdict" and confirms success from the backup artifacts on disk (logging that it did so); a job that vanishes without producing its files still fails, now with an error naming the missing files instead of looping for 2 hours. - **`restoring domain state: Failed to terminate process … with SIGKILL: Device or resource busy` after VM backups** (#160). After pushing tens of GB through the page cache (typical on fuse-backed Unraid shares), the temporary paused backup session's qemu process can sit in uninterruptible IO, so libvirt's single kill attempt timed out, failed the whole backup, and could leave a cold-backed-up VM powered off. Teardown of the backup session (and the forced stop before a cold backup) now retries the destroy for up to 5 minutes until the flush completes, then restarts the VM as before. - **qcow2 disks in `.img` files are now backed up and restored in their real format** (#160). Disk format was inferred from the file extension, but Unraid names most vdisks `vdisk1.img` regardless of format. A qcow2-formatted `.img` (like the Windows VM in the report) was written out as a raw image and restored as raw while the domain XML still said qcow2 — an unbootable restore. The format now comes from the domain XML's `` (extension only as fallback), which also makes qcow2-in-`.img` VMs eligible for incremental backups for the first time. - **VM restore-chain assembly no longer assumes every chain layer ends in `.qcow2`.** Restoring an incremental chain resolves each step's disk files from the `vm_meta.json` the backup recorded (with the old filename scan as fallback for pre-existing backups), so chains whose full backup is a `.img` file flatten correctly; a chain whose disks cannot be resolved now fails loudly instead of silently restoring only the newest increment. `qemu-img` calls also honour restore cancellation, and layer/metadata filenames are validated before use. - **A VM whose disks are all skipped (e.g. block-passthrough only) now fails its backup with a clear error** instead of "succeeding" with no disk data. The per-VM disk inventory (eligible disks, formats, and skipped disks with the reason) plus the exact backup job submitted to libvirt are now logged, so future reports like #160 carry the root cause in `vault.log`. - **Restore wizard no longer shows `no tar index sidecar found for this item` for VMs.** The per-file restore picker only applies to tar-based backups (containers, folders, plugins); for VM and ZFS items the wizard showed a red sidecar-lookup error that looked like a broken restore point. Those items now show a plain "Restored in full" row, and the disabled **Start Restore** button explains that the pre-flight checks must be run first. ## [2026.06.07] - 2026-06-25 ### Changed - **Modals no longer close when you click the background** (closes #148). Clicking the dimmed backdrop behind a dialog – or releasing a text selection whose drag ended outside the dialog – used to dismiss the modal and discard any in-progress work, such as a half-filled backup job. Backdrop click-to-close has been removed from every modal: the job and storage wizards, confirmation dialogs, the command palette, and the path and script browsers. Modals are still dismissed the deliberate ways – the close (×) button, Cancel, or the Escape key – all unchanged. ### Fixed - **Creating a backup job now validates its input instead of saving a broken job or returning a 500.** `POST`/`PUT /api/v1/jobs` previously performed no validation: a request with no name (or a whitespace-only name) was saved as an unusable job, and a request with an invalid cron expression was saved as a job that silently never ran. The job create/update endpoints now reject an empty name with `400 name is required` and an unparsable schedule with `400 invalid schedule: …` (an empty schedule is still allowed — that means a manual-only job), matching the validation the storage and replication endpoints already had. Last-day-of-month schedules (`0 2 L * *`) and `@`-descriptors remain valid. - **Duplicate names now return a clear 409 instead of a generic 500.** Creating or renaming a job, storage destination, or replication source to a name that already exists tripped the underlying `UNIQUE` constraint and surfaced as `500 internal server error`. These now return `409 Conflict` with an actionable message (e.g. _"a job with this name already exists"_) so the UI and API clients can show the real reason. - **Container appdata on Unassigned Devices is no longer auto-excluded from backups** (closes #152). Bind mounts under `/mnt/disks/` (the Unassigned Devices mount point) were wrongly flagged as a "direct disk volume" and shown greyed-out in the job wizard's **Container Mounts & Exclusions** step, so appdata kept on an Unassigned Devices SSD (e.g. a container's `/config` at `/mnt/disks//appdata/`) could not be included in a backup. The direct-disk rule now matches only real array disks (`/mnt/disk1`, `/mnt/disk2`, …) and no longer catches the plural `/mnt/disks/`, so those mounts are backed up by default and are tickable like any other path. ## [2026.06.06] - 2026-06-22 ### Added - **Richer Discord notifications: bot identity and role pings** (#146). The **Settings → Notifications → Discord** card now lets you override the **Bot Username** and **Bot Avatar URL** the webhook posts under, and ping a **Mention Role ID** on alerts with a **Mention On** control (Never / Failures only / All backups). Pings are deliberately safe — only the role you specify is mentioned (`allowed_mentions` is locked down, so a stray mention can never `@everyone`), the role ID is validated as a numeric snowflake before use, and the **Test** button now reflects your configured bot name/avatar. By default nothing pings (Mention On = Never), so existing setups are unchanged. ### Fixed - **You can now choose to be notified on successful backups, not just failures** (closes #146). The per-job notification preference (`notify_on`) was already honoured by the backend and submitted by the job form, but the job wizard never rendered a control to change it, so it was stuck at its `failure`-only default and there was no way to opt into success notifications. The job wizard's **Step 5 → Advanced** now has a dedicated **Notifications** section (split out from Scripts for clarity) with a **Notify On** dropdown — **All backups (success & failure)** / **Failures only** / **Never** — and a tooltip noting that the global notifications toggle in Settings must be on and that Discord is configured separately. No backend, database, or API changes were required. ## [2026.06.05] - 2026-06-20 ### Added - **Configurable history retention.** A new **Settings → General → History Retention** control (30 days / 90 days / 6 months / 1 year / 2 years / Keep everything; default 1 year) caps how long backup/restore run history is kept, so the database doesn't grow without bound. The purge (which runs during the daemon's maintenance pass) is deliberately safe: it only removes run-history rows that are both older than the chosen period **and** no longer back a restore point – recoverable backups are governed solely by each job's own backup retention and are never deleted by this setting. - **Selectable backup-size trend window.** The History page's "Backup Size Trend" chart now has a period selector – **7d / 30d / 90d / 6m / 1y** (default 30d) – instead of a fixed last-30-runs view. Long ranges are aggregated server-side into readable buckets (per run for 7d, per day for 30d/90d, per week for 6m/1y) and the bars are stacked by backup type (containers / VMs / folders / flash). Backed by a new `GET /api/v1/history/trend` endpoint. - **Capacity trend and runway forecast on storage destinations.** Each destination's capacity block now shows a sparkline of usage over the last 90 days (the samples Vault already collects) plus a plain-language runway estimate – "~N days until full at current growth", "Usage stable or shrinking", or "Not enough history yet" – computed from a linear fit of recent samples. The trend and runway colour-shift to amber/red as the destination nears full. Quota-less providers (S3, generic WebDAV) show the usage trend without a percentage or runway. - **Dedup savings bar.** Deduplicated destinations now show a visual savings bar beside the existing ratio text: how much of the logical data is physically stored versus how much deduplication avoided writing (e.g. "Stored 29% · Saved 70.6 GB"). - **Search, filter, and sort your backup jobs.** The Jobs page now has a toolbar above the list: a **search** box (matches name and description), a **status** filter (All / Enabled / Disabled), a **storage** filter (any destination), and a **sort** control (Name A–Z, Next run, Recently created). A "Showing X of Y" counter and a **Clear filters** link appear when a filter is active, and the existing **Select all** plus bulk actions (enable / disable / run / delete) now operate on the _filtered_ set – so you can, for example, filter to Disabled and bulk-enable just those. All client-side over the already-loaded list, so there is no extra loading. Useful once you have more than a handful of jobs. - **Pre-flight checks before a restore.** The restore wizard's final step now runs a quick go/no-go check before you commit: **Storage reachable** (the destination answers), **Backup present on storage** (the restore point's data is actually there), **Decryptable** (the supplied passphrase matches, for age-encrypted jobs – skipped otherwise), and **Free space** (the staging/destination volume has room for the restore point). **Start Restore** stays disabled until the checks pass, so a doomed restore fails in a second instead of part-way through. Changing the passphrase or destination re-arms the checks ("Inputs changed since the last check"); when checks fail you can still **Restore anyway** behind a confirm. Backed by a new `POST /api/v1/jobs/{id}/restore-points/{rpid}/preflight` endpoint. - **Restore points now show as a visual timeline.** The restore wizard's version-selection step replaces the flat card list with a day-grouped timeline plus a density overview (one bar per day, sized by backup volume) so you can see at a glance what you can recover and from when. Each point shows its time, type (full/incremental/differential dot), size, and the trust signals Vault already computes – **Recommended** (newest), **Verified**, and chain health (broken / chain depth / retained). Calm, keyboard-navigable, and responsive. - **The 3-2-1 Backup Rule panel is now goal-aware.** Instead of always scoring against the full rule (which left a deliberately simple setup permanently red), you pick a backup goal that matches your intent and the panel scores against that: **Local only** (at least one copy), **Local + offsite** (a local copy plus a remote/replicated copy), or **Full 3-2-1** (3 copies, 2 media types, 1 offsite). It turns green when the chosen goal is met and only suggests the steps relevant to that goal. The goal is a segmented control in the panel; when unset it auto-selects the highest tier your current setup already meets. Persisted in a `backup_rule_goal` UI setting (frontend-only). Works alongside the show/hide toggle. - **The 3-2-1 Backup Rule panel can now be hidden.** The Dashboard's 3-2-1 compliance panel is useful guidance, but for an intentionally simple setup (e.g. a single server backing up locally) it stays permanently red, which reads as a failure rather than a choice. A new **×** on the panel hides it instantly, and a **Settings → Dashboard → Show 3-2-1 Backup Rule** toggle re-enables it. It remains shown by default, so the best-practice nudge is still there for anyone who wants it. No scoring changes. - **Per-mount include/exclude toggles for container backups** (closes #143). The job wizard's container section (now **Container Mounts & Exclusions**) lists every bind mount of each selected container with a checkbox – uncheck a mount to keep its data out of the backup (e.g. a media or downloads share mapped into Sonarr/Radarr/Plex). This replaces the slow, error-prone workflow of hand-typing each container path per container. Mounts Vault already auto-skips (device/virtual paths and known shared-data shares) are shown disabled with the reason, so it's clear what will and won't be backed up. A new `GET /api/v1/containers/{name}/mounts` endpoint exposes the mount list with each mount's auto-skip verdict. The free-text exclusion box is preserved below as **Additional path exclusions** for subpaths and glob patterns (e.g. `/config/Cache`, `*.log`); a checkbox and an exact whole-mount text exclusion stay in sync, so pre-existing exclusions (such as a `/rootfs` entry on Telegraf/Glances) correctly show their mount as unchecked. ### Fixed - **The History "Purge" button now warns it deletes restore points.** Purging run history runs a cascade that also removes the restore points those runs produced – the recoverable backups Vault tracks – leaving their files orphaned on storage. The confirmation dialog previously said only "delete all job run history records"; it now spells out that restore points go too and that this cannot be undone, so it isn't triggered by surprise. - **Adding an item to a job no longer claims it's already backed up.** When you added a container or VM to an existing job, it immediately showed as **protected** on the Dashboard and appeared in the Restore tab with the job's whole backup history (e.g. "backed up today, 5 restore points") — even though that backup had not run yet, and trying to restore it returned **"internal server error"**. The cause: protection and restore-point listing keyed off _job membership_ rather than what each backup actually captured. Vault already records exactly which items each restore point contains, so it now uses that: such an item shows as **"Pending first backup"** (a distinct amber state, not green-protected) until a backup really captures it; the Restore tab only lists restore points that contain the selected item; and browsing or restoring an item a restore point never captured now returns a clear message instead of a 500. The health ring's protection figure is likewise based on real per-item membership. _(Items backed up before this metadata existed are treated as before.)_ - **The Dashboard's Backup Health card no longer contradicts itself.** The ring (operational health: backup success plus protection of the items you've put in jobs) could read a green **100%** while its own subtitle said "Attention needed – 12 items unprotected" – because the subtitle was secretly using a different metric (whole-server coverage, e.g. 8/20). The subtitle now describes the same thing the ring does ("All backups healthy"), and overall coverage is shown as a calm, non-alarming hint ("12 items not in any backup job · see Protection Status") that points at the Protection Status panel, which owns that metric. A deliberately partial setup (backing up only some containers) now stays green instead of looking broken. - **Consistent save buttons across Settings.** Every primary action button now behaves the same while working: it shows an inline spinner next to its label (previously only some did – the encryption, API-key, retry-policy, compaction, and staging/snapshot "Apply" buttons just swapped text or did nothing visible). Also fixed seven Settings buttons that used an undefined `hover` colour and so had **no hover effect at all**, normalised two others that used a different hover shade, and standardised the busy-label punctuation (`Saving…` rather than a mix of `Saving...` and `Saving…`) and font weight. - **Storage destination cards no longer look uneven.** Within a row, the cards already stretched to equal height, but each card's footer (timestamp · job count · health · Test) floated at a different vertical position, leaving a large empty gap under shorter cards. The footer is now pinned to the bottom of every card so the rows line up. - **Disabling anomaly detection now hides it on backup jobs too.** With anomaly detection turned off in Settings, the **Learning baseline** badge (and the per-job anomaly badge and the job editor's anomaly-sensitivity override) still appeared on backup jobs. The Jobs page now gates all anomaly UI on the global toggle, matching the Dashboard and the Anomalies page, so disabling anomaly detection hides it everywhere until it is re-enabled. - **Selecting a specific NIC bind address now works end-to-end** (closes #139). Cycling the **Bind Address** to a detected interface IP (e.g. `192.168.20.21`) left the daemon showing **STOPPED** and made the Vault UI unreachable – only `127.0.0.1` and `0.0.0.0` worked. Two causes are fixed: (1) `rc.vault` validated the address with a bare `ip` command, but the service/php-fpm execution context often lacks `/usr/sbin` in `PATH` (the same gap fixed for `apply.sh` in #136), so a valid NIC address silently failed validation and was reset to `127.0.0.1` _in memory_ – the daemon bound loopback while the UI health probe targeted the NIC IP. `rc.vault` now resolves the `ip` binary to an absolute path and, when `ip` is unavailable, accepts the configured address rather than rejecting it. (2) Once bound to a NIC IP, every request from the co-located Unraid PHP proxy arrived from a non-loopback source and was rejected with **401 "valid API key required"** when an API key was configured – breaking the _entire_ proxied UI, not just the status check. The daemon now exempts requests originating from the host itself (loopback **or** any local interface address), so the local proxy is trusted on any bind address while genuine remote LAN clients still require a key. - **The service status no longer shows a false STOPPED right after applying a configuration change.** A restart that takes the hybrid database-restore path can need ~15–20 s to bind and serve health; the post-Apply status poll gave up too early and reported STOPPED until a manual refresh. The poll window now extends to ~36 s. - **Deleting jobs no longer makes the daemon appear to hang** (issue #143). After any configuration change (job/storage/settings CRUD) the daemon flushes its database to the USB flash drive; this flush ran **synchronously on the request**, so deleting several jobs in a row serialised multiple multi-second flash writes and the UI froze (often mistaken for a crash). The flush is now scheduled off the request path and **coalesced**, so a burst of deletes triggers a single flush and each delete returns immediately. Shutdown durability is unchanged – a synchronous flush still runs before the daemon exits. - **"Delete all files" cleanup no longer fails when a backup directory is already gone** (issue #143). Removing a job or restore point with _delete files_ could report **"failed – files may remain on storage"** purely because a directory it tried to list no longer existed (e.g. a WebDAV `PROPFIND` returning `404` for an already-removed or never-written path). A missing directory now counts as already-clean instead of aborting the whole cleanup. - **Deleting a job on a deduplicated destination now reclaims its storage** (issue #143). Dedup backups share a single content-addressed repository (`_vault/`) across every job on a destination, so deleting one job previously left its now-orphaned chunks behind. Deleting a job now runs garbage collection to free the chunks no longer referenced by any surviving job, and when the **last** dedup job on a destination is removed the entire `_vault` repository (and its index) is deleted outright. - **WebDAV backups no longer fail with a transient `423 Locked` while creating folders.** Some WebDAV servers (notably Koofr) briefly lock a directory during `MKCOL` – especially right after a failed upload's partial files are cleaned up – so the very next `MkdirAll` returned `423` and failed the whole item (e.g. _"writing config.json to storage: … MkdirAll …/telegraf/: 423"_). Directory creation now retries transient WebDAV errors (`423`, `409`, `429`, `5xx`, network blips) with the same backoff already used for chunk uploads, so a momentary lock clears and the backup proceeds. Verified end-to-end against a live Koofr destination. ### Changed - **Dashboard VMs card reads "Virtual Machines" instead of "libvirt".** The stat card's subtitle showed the implementation name (`libvirt`); it now uses the friendlier "Virtual Machines" label. - **Item picker empty states follow Unraid's service naming.** When adding items to a job and the relevant service is off, the message now reads "Docker service not available" / "Virtual Machine service not available" instead of "Docker is not available" / "libvirt is not available". - **Configuration form on the Unraid plugin settings page is now left-aligned.** The **Daemon Port** and **Bind Address** labels and fields previously sat in an indented, right-aligned column (Unraid's default `dl/dt` styling); they now sit flush left in a clean label/field layout, matching the Unraid Management Agent plugin's Configuration page. - **Em dashes replaced with en dashes throughout the UI.** Swept the Svelte/JS web UI, the Unraid plugin settings page (including the Bind Address options), and these release notes (which render in the in-app View Changelog modal) to use en dashes instead of em dashes for a cleaner, more consistent look. - **Bind Address dropdown is grouped and no longer stretches across the screen.** The list is organised into labelled sections – _Local only (recommended)_, _All interfaces_, and _Network interfaces_ (detected NICs shown as ``) – and the control is given a fixed, compact width so the Configuration form stays left-aligned instead of spanning the full width of wide displays. - **Applying a configuration change reports its result inline instead of as raw script output near the footer.** Previously the Apply form streamed `rc.vault`'s raw output (e.g. "Stopping Vault daemon…") into Unraid's progress area just above the **Array Started** system-status bar. That output is now suppressed and replaced with a co-located "Applying configuration – restarting daemon…" note next to the form that resolves to the live RUNNING/STOPPED status – keeping the daemon's feedback in-component rather than in Unraid's reserved system-status area, consistent with Unraid plugin conventions and the official Unraid API plugin. - **The Dashboard's Protection Status panel grows to fit its contents** (issue #143). Each category column (Containers, VMs, Folders, Flash Drive) was capped at a fixed height with an inner scrollbar, so servers with many containers had to scroll within a small box. The columns now size to the number of items, showing every container/VM at a glance without a nested scrollbar. ### Removed - **The API Endpoints list was removed from Settings → Reference.** It enumerated every REST route in the UI, which added clutter without real value for operators (no comparable backup tool surfaces its API inside the app), so it has been dropped. The Reference tab keeps its Compression Guide and Backup Type Guide. Only the in-UI list was removed – the REST endpoints themselves are unaffected, and integrations that call `/api/v1/...` continue to work. ## [2026.06.04] - 2026-06-13 ### Added - **Network auto-discovery via mDNS/zeroconf** (closes #137). The daemon now advertises itself on the local network as a `_vault._tcp` service (with `version` and `tls` TXT records), so integrations such as the Home Assistant Vault integration can auto-discover a running Vault instance instead of requiring a manually entered address. Discovery is automatically **disabled** when the daemon is bound to a loopback address (the local-only default) – a `127.0.0.1`/`::1` bind is never advertised – and enabled for all-interfaces (`:24085`, `0.0.0.0`, `::`) or specific-NIC binds. Pure-Go and Avahi-compatible; advertisement failures are non-fatal and never block backups. - **Hide unused services for a cleaner interface.** Anomaly Detection and Replication can now be toggled off from **Settings**. Disabling a feature removes it from the sidebar (and the Dashboard, for Anomaly Detection) and halts its backend work – anomaly evaluation already stops, and scheduled replication syncs are now paused while disabled. Replication defaults to visible only when replication sources are already configured, so existing setups are unaffected and fresh installs stay uncluttered. Replication is always shown on replica instances (it is core there). Re-enabling a feature restores its page and resumes its backend work immediately, without a daemon restart. ### Fixed - **Backup jobs now fail when all of their targets are missing** (closes #138). When every container/VM/folder/plugin/dataset configured for a job no longer exists on the server, the run previously reported **completed** with zero items backed up, masking the fact that nothing was protected. The runner now records a **failed** run with a clear error listing the missing targets (and counts them as failed items) so the failure is visible in the UI, notifications, and the reliability detector. Because a missing target is a configuration problem rather than a transient/destination fault, this does not schedule a retry or trip the destination circuit breaker. A safety net also fails any run that ends with zero items processed despite items being configured. - **The Dashboard's Recent Activity now shows the real failure reason instead of "expand for details"** (follow-up to #138). The failure-reason helper only surfaced plain-text log lines containing the words "error"/"fail", so messages that contained neither (such as the all-targets-missing message) fell through to a generic "Backup failed – expand for details" – misleading on the Dashboard, which has no expand affordance. It now surfaces the first meaningful line of any plain-text log, and the remaining placeholder reads "see Logs for details". The Recent Activity failure line also now **wraps** (up to three lines, full text on hover) instead of truncating to a single cut-off line. - **Settings "Save" buttons no longer balloon and show "Loading…" while saving.** The Anomaly Detection, Storage Logging, and Discord Save buttons (and the Discord Test / diagnostics buttons) embedded the full-section loading spinner – which carries large padding and a "Loading…" label – directly inside the button. They now use a small inline spinner that keeps the button's shape and colour. ## [2026.06.03] - 2026-06-12 ### Changed - **S3 key sanitization is no longer silent.** When a job or item name contains characters that must be replaced for S3-compatible gateway signing (e.g. spaces – `QA S3 verify` is stored as `QA_S3_verify`), the daemon now logs the original → stored mapping once per session, so operators browsing the bucket can connect what they see to their job names instead of discovering the rename by accident. ### Fixed - **The Unraid Plugins page now shows "Vault" with a proper description.** The plugin list previously showed a lowercase **vault** with no description because Unraid's plugin manager renders `/usr/local/emhttp/plugins//README.md` for the title and description, falling back to the raw lowercase plugin name when that file is missing. The plugin package (and the dev deploy) now ship that README – title-cased name plus a one-paragraph summary – matching how other plugins present themselves. - **The Unraid Plugins page now shows "Vault" with a proper description.** The plugin list previously showed a lowercase **vault** with no description because Unraid's plugin manager renders `/usr/local/emhttp/plugins//README.md` for the title and description, falling back to the raw lowercase plugin name when that file is missing. The plugin package (and the dev deploy) now ship that README – title-cased name plus a one-paragraph summary – matching how other plugins present themselves. - **Importing backups now skips manifests written by a newer Vault version with a clear log message.** Previously a manifest with an unrecognised (future) schema version was silently parsed as a legacy manifest, which could import a backup with wrong or missing settings. Scan and import now check the manifest version and skip too-new manifests with an explicit "upgrade Vault to import this backup" log line; current and legacy manifests import unchanged. - **Bind Address dropdown now lists detected NIC addresses** (closes #136). The per-interface addresses (e.g. `192.168.20.21 (br0)`) never appeared because PHP silently casts the numeric-string `'4'`/`'6'` array keys in `vault_get_local_ips()` to integers, so the strict family comparison never matched and every detected address was filtered out. The keys are now non-numeric (`v4`/`v6`), and the `ip` binary is resolved to an absolute path (`/usr/sbin/ip`, fallback `/sbin/ip`) since the webgui's php-fpm PATH may not include `/usr/sbin`. The loopback and wildcard options (`127.0.0.1`, `::1`, `0.0.0.0`, `::`) are always present regardless of detection. The Apply-time bind-address validation in `apply.sh` uses the same absolute-path resolution and no longer resets a valid NIC address to `127.0.0.1` when the `ip` binary cannot be found. - **Activity Log and Replication pages no longer flicker every few seconds** (closes #135). When the UI runs through the Unraid plugin proxy it refreshes by polling (5 s on Logs, 10 s on Replication), and every poll swapped the whole list out for a loading spinner and back – a visible jitter/flicker on each tick. Background refreshes now update the list in place; the spinner only shows on first load and explicit user actions (filter change, Refresh). A failed background poll also keeps the current list on screen instead of flashing an error panel, and on Replication it no longer raises an error toast every tick. - **Restoring a chunked WebDAV backup no longer fails with a 404.** Restore downloads stream every object through ranged reads (the resumable reader introduced with resilient restores), but the WebDAV adapter's ranged read did not understand chunked (sidecar-manifest) uploads – and a chunked upload has no object at its logical path at all – so restoring any WebDAV backup larger than the chunk size (default 50 MiB) failed immediately. Ranged reads now detect the chunk manifest and serve the requested byte window from only the chunks that overlap it, restoring both the sequential resumable path and the parallel ranged-download path for chunked objects. Plain objects (including dedup pack files) are unaffected and keep their single-request fast path. - **Background backup-file cleanup now reports its outcome** (follow-up to #111). Deleting a job or restore point with its backup files returns immediately while the remote sweep runs in the background, but a failed sweep was only visible in the daemon log. A failure is now recorded in the Activity Log (with the storage error) so it is durably visible, and the Jobs page shows a toast when the sweep for a deleted job completes or fails. - **Timed-out finalization steps are now cancelled instead of leaking** (follow-up to #112). When a post-backup finalization step (manifest write, database backup, retention sweep) exceeded its hard timeout, the runner moved on – correct – but the hung step's goroutine kept running forever, holding its storage connection open. Each step now receives a context that is cancelled on timeout: in-flight storage I/O is aborted by closing the step's adapter, and the retention and database-backup sweeps stop between items, so the abandoned goroutine exits promptly. A cancelled retention sweep simply resumes on the job's next run. - **Dedup backups can no longer be falsely cancelled by the stall watchdog during long pack uploads** (follow-up to #110). The dedup (chunked) backup path only refreshed the no-progress watchdog once per completed file, so a single very large file uploading packs to a slow remote for over two hours could be cancelled mid-transfer. Pack uploads now heartbeat the watchdog on every chunk of bytes written, matching the classic upload path. ## [2026.06.02] - 2026-06-10 ### Fixed - **Copy API key works on plain-HTTP (non-secure) origins** (closes #129). The **Copy** button under Settings → API Key failed with `Failed to copy – clipboard access denied` in Safari and Chrome because the modern Clipboard API is only available in secure (HTTPS) contexts, and the Unraid web GUI is typically served over plain HTTP on the LAN. Copying now falls back to the legacy `execCommand('copy')` path via a shared clipboard helper when the Clipboard API is unavailable or denied. The Activity Log's per-entry copy button uses the same fallback. - **Changing the Bind Address (or Port) now reliably restarts the daemon when Apply is clicked** (closes #130). The plugin's `apply.sh` only restarted the daemon when the PID file was present and valid – a stale or missing PID file silently skipped the restart, leaving the daemon on the old address until a manual restart. It now falls back to `pidof` (the same process lookup `rc.vault` uses), prints an explicit `Restarting Vault daemon...` line in the Apply progress window, and probes the post-restart health check against the actual configured bind address (previously hardcoded to `127.0.0.1`, which could not confirm a daemon bound to a specific NIC address). ### Changed - **VM and folder backup progress now shows human-readable sizes** (closes #133). Live progress messages such as `backup in progress: 6453198848/34359738368 bytes` now read `backup in progress: 6 GB/32 GB`. The same formatting applies to VM disk restore, NVRAM copy, stale block-job wait, and the folder-backup manifest completion message. - **Anomaly detail values are now human-friendly** (closes #134). Anomaly details stored and shown in notifications, the API, and the UI previously carried full float precision (e.g. `Z Score: -16.76413455138884`, `Growth Factor: 0.5766537578335602`). New anomalies are rounded at the source (z-score and growth factor to 2 decimals, capacity ETA to 1 decimal, slope/intercept to whole bytes, free-space percentage to 1 decimal), and the Activity Log formats these fields at display time so entries recorded before this change read cleanly too. ## [2026.06.01] - 2026-06-08 ### Changed - **Renamed "Grandfather-Father-Son (GFS)" retention to "Long-Term Retention (LTR)"** across the UI, code, and docs (closes #125). LTR is the industry-standard enterprise term for keeping weekly/monthly/yearly recovery points (as used by Veeam, Druva, and others) and reads far more clearly than the legacy tape-rotation name. This is a **terminology-only** change – no database, API, or behaviour changes: the `keep_latest`/`keep_daily`/`keep_weekly`/`keep_monthly`/`keep_yearly` job fields and the `GET /jobs/{id}/retention-preview` endpoint are unchanged. - **Collapsible API reference** (closes #126). The API Endpoints list under Settings → Reference is now split into collapsible sections (Health & Realtime, Jobs, Storage Destinations, Deduplication, and so on), each showing its endpoint count and collapsed by default, so the reference stays tidy and easy to scan as the API grows. - **Clearer help tooltips.** Rewrote the tooltips for the more technical storage and settings options – WebDAV/S3 transfer tuning (chunk/part size, stall and request timeouts, path-style, bandwidth), dedup compaction threshold, database mode, and scheduled/backup verification – into plain language with a consistent _what it does → why it matters → recommended default_ structure. No behaviour change. ### Fixed - **Changing the daemon port no longer silently breaks the web UI** (closes #124). Previously an invalid or already-in-use port was written straight to `vault.cfg` (the port field's `1024-65535` limit was enforced only client-side), the daemon failed to bind and exited, and the UI became unreachable with no feedback. The plugin's `apply.sh` now validates the port server-side – rejecting non-numeric or out-of-range values and resetting to the default `24085` – then verifies the daemon actually comes back up after the restart and prints a clear diagnostic (with the tail of the log) if it does not. `rc.vault` now re-checks the process for a few seconds after launch and dumps the last log lines on a failed start instead of leaving a stale PID file. The Settings page polls the daemon's real status after an Apply and shows a visible error (instead of only a transient progress frame) when the chosen port can't be bound. ## [2026.06.00] - 2026-06-07 ### Fixed - **Large backups no longer fail with `dedup: pack size … exceeds safety bound` when an item contains a very large number of files (#118).** Backup manifests (the per-item index of every file and its chunks) were stored as a single JSON blob; for a directory with an enormous number of files – e.g. a Plex appdata `/tmp` tree – that blob grew past the packer's 96 MiB safety bound and the whole backup aborted. Oversized manifests are now split through the same content-defined chunker used for file data and stored as ordered segments behind a small envelope, so no single chunk approaches the bound regardless of file count. The split is transparent on restore, existing (v1) single-blob manifests still read unchanged, and because near-identical manifests now mostly re-reference existing segments, manifest storage also deduplicates across snapshots. A clearer early error also replaces the opaque packer failure if any oversized chunk is ever stored. - **Anomaly values are now human-readable, and the "Raw details" toggle works.** Anomaly summaries and the detail panel showed raw numbers – e.g. `backup size anomaly: 4258918530 bytes`, `Observed: 4259532913`, `Deviation: -27.28833643436231` – and clicking **Raw details** did nothing. Values now adapt to friendly units (bytes → KB/MB/GB, seconds → `5m 26s`, ETA → days) on both the summary line and the Observed/Expected fields, and the deviation z-score is rounded to two decimals. The **Raw details** disclosure was a `
` nested inside the row's `