# Changelog All notable changes to the Unraid Management Agent will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- ## [Unreleased] ## [2026.10.02] - 2026-10-20 ### Fixed - **Parity schedule endpoint lost week of month and last day of month selectors** (#281) — `GET /api/v1/array/parity-check/schedule` parsed `dotm` from `dynamix.cfg` with `strconv.Atoi`, dropping non-numeric selectors like `W1`–`W4`, `WL` (last week) used in weekday-based schedules (Monthly, Yearly, and Custom modes), `*` (every week in Custom mode), and `28-31` (last day of month in Monthly and Yearly modes). Because `check_cron` extracts the five standard cron fields without Unraid's shell guards (e.g. `[[ $(date +%e) ... ]]`), schedules on different weeks or the last day returned identical data and day 0. The endpoint now exposes optional `week_of_month` (`"W1"`–`"W4"`, `"WL"`, and `"*"` for Custom mode) and `last_day_of_month` (`true` for Monthly/Yearly `28-31` schedules) while keeping the numeric `day_of_month` contract unchanged. Both fields are gated on the final resolved mode to omit stale selectors when modes change. Swagger and REST documentation have been updated, and `check_cron` documentation corrected to clarify that shell date guards are excluded. - **Registration reported valid licenses as expired** — `/api/v1/registration` read `regTm`, the date the key was registered, as both the license expiration and the update expiration, so every key registered before the current moment showed `state: expired`. Expiration now comes from `regTm2` for trial and expired keys only, and update expiration from `regExp`, matching how Unraid's webGui reads them. Basic, Plus, Pro and Lifetime keys report `valid` with no expiration, an active trial reports `trial`, and a lapsed update period never changes `state`, so a Starter or Unleashed key with lapsed updates stays `valid`. ([#285](https://github.com/tinkerologie/unraid-management-agent/issues/285)) ## [2026.10.01] - 2026-10-18 ### Added - **Dynamic collection mode** (#280) — `COLLECTION_MODE="dynamic|manual"` (`--collection-mode`, `collection_mode` in `config.yml`). In Dynamic mode the agent decides when each collector runs instead of the 21 interval settings: - **Change events** run collectors right away: Docker container events, libvirt VM lifecycle events, file changes under `/var/local/emhttp` and `/boot/config` (array, disks, shares, notifications, mover, Unassigned Devices, plugin installs) and kernel hotplug uevents. Every event-driven collector keeps a slow safety-net poll. - **Demand-aware cadence**: a collector runs at its active rate while a WebSocket client is subscribed to one of its topics, MQTT is connected, or its data was read over REST or MCP in the last 5 minutes, and drops to its idle rate otherwise. Clients subscribed only to other topics do not speed it up. - **Refresh on read**: for demand-following collectors, a REST, MCP or `/metrics` read of data older than the collector's staleness limit triggers one shared collection and waits up to 2 s for it. If the collection fails or is late the cached data is returned; REST responses carry `X-Cache-Updated-At` and `X-Cache-Stale`. Collectors on long fixed schedules (update checks, license, tuning, hardware info) are not refreshed by reads. - **Safety floors**: while an enabled alert rule reads a collector's data, it never runs slower than its Manual default. The 15 s alert evaluation, health checks and fan control are unchanged. - `GET /collectors/status` reports `mode`, `effective_interval_seconds`, `reason` (`event`, `active`, `idle`, `floor` or `manual`), `event_driven` and `publish_count`. In Dynamic mode `PATCH /collectors/{name}/interval` (and the MCP `update_collector_interval` tool) returns a conflict. - **"Dynamic collection" toggle** on the plugin settings page. When it is on, the interval settings below it (the interval grid, Show all collectors, Re-check hardware and the power tip) are hidden; saved intervals are kept and apply again in Manual mode. Hardware detection keeps gating the hardware collectors in both modes. ### Changed - Fresh plugin installs start in Dynamic mode. On upgrade, installs whose intervals are all shipped defaults move to Dynamic; installs with any changed interval stay on Manual, and no interval is changed. The daemon outside the plugin still defaults to Manual. ## [2026.10.00] - 2026-10-18 ### Added - **VM guest agent readings** — VMs listed under `vm_guest_agent` in `config.yml` (or `--vm-guest-agent` / `VM_GUEST_AGENT`) are read through the QEMU guest agent: host name, OS, filesystem usage, NVIDIA GPU readings from `nvidia-smi` and, on Windows, disk temperature and wear from `Get-StorageReliabilityCounter`. Custom sensors run a command inside the VM on their own interval and report its output. Served at `GET /api/v1/vm/guest` and in the new `vm_guest_update` WebSocket event. While a VM is off its GPUs, disks and sensors stay listed with `null` readings, so consumers can show them as unknown rather than unavailable. A custom sensor with `json: true` expands a flat JSON object printed by its command into one sensor per key, named `_` (for example `frames_avg`); output that is not a JSON object counts as a failed run and the known keys stay listed with `null` values. `attributes` (for example `graph: avg`) turns a key into an attribute of another key's sensor instead of a sensor of its own. - **Seven more collector intervals on the plugin settings page** — `INTERVAL_FAN_CONTROL` (60 s), `INTERVAL_TUNING` (120 s), `INTERVAL_DOCKER_UPDATE` (6 h), `INTERVAL_DOCKER_NETWORKS` (60 s), `INTERVAL_PLUGIN_UPDATE` (1 h), `INTERVAL_OS_UPDATE` (24 h) and `INTERVAL_MOVER` (30 s) are now in `default.cfg`, the first-install config, the start script (default, validation and passthrough to the daemon) and the settings page; Disabled = 0. The daemon already supported them, but the plugin dropped them, so they could not be changed from the UI. - **UPS telemetry: voltages, frequency, temperature, transfers and self-test** — `/api/v1/ups` (and the `ups_status_update` event) now reports `input_voltage`, `output_voltage`, `battery_voltage`, `line_frequency`, `temperature_celsius`, `time_on_battery_seconds`, `cumulative_time_on_battery_seconds`, `transfer_count`, `self_test` and `serial_number`. From apcupsd: `LINEV`, `OUTPUTV`, `BATTV`, `LINEFREQ`, `ITEMP`, `TONBATT`, `CUMONBATT`, `NUMXFERS`, `SELFTEST`, `SERIALNO`; from NUT: `input.voltage`, `output.voltage`, `battery.voltage`, `input.frequency`, `ups.temperature`, `battery.runtime.elapsed`, `input.transfers` (only drivers that report this non-standard variable), `ups.test.result`, `device.serial`/`ups.serial` (NUT has no cumulative on-battery time). Numeric readings the UPS does not report are `null`, never `0`; `self_test` (the raw backend value) and `serial_number` are omitted when unknown. `LINEV`, `BATTV`, `input.voltage` and `battery.voltage` were parsed before but discarded. ([#193](https://github.com/tinkerologie/unraid-management-agent/issues/193)) - **Hardware auto-detection for the settings page and collectors** — the agent now detects which optional hardware the server has and runs the matching collectors only when it is present. Seven read-only hardware classes join the capability snapshot: `hardware.nic_optics` (a wired PCI NIC with a hwmon temperature sensor or a plugged SFP/QSFP module; plain onboard NICs such as I219-V and I211 and Wi-Fi cards such as the 3168NGW do not count), `hardware.storage_topology` (an SES enclosure, an SES SCSI generic device, a SAS HBA or a Broadcom/LSI RAID controller), `hardware.gpu`, `hardware.ups`, `hardware.nut`, `hardware.zfs` (an imported pool) and `hardware.fan_control`. Each has a `present`/`absent`/`unknown` state and a reason. Checks read sysfs, procfs and config files only: no commands, no disk access, and at most 16 module EEPROM ioctls when no NIC sensor exists. Capabilities are re-checked at startup, every 15 minutes and on demand. New read-only endpoint `GET /api/v1/capabilities` (`?refresh=true` re-checks, at most every 30 s); the capability items gain additive `state` and `checked_at` fields, also in the self-test. ([#261](https://github.com/tinkerologie/unraid-management-agent/issues/261)) - **Hardware-gated collectors and Auto mode** — the NIC, Storage Topology, GPU, UPS, NUT and ZFS collectors now follow hardware detection: while their hardware is absent they are off and their settings rows are hidden, whatever interval is saved; the saved value applies again once the hardware is detected (hardware seen before must be absent twice in a row; `unknown` changes nothing). Their interval selects gain an **Auto** choice that runs the collector every 300 s (1200 s in low power mode) while the hardware is present, passed to the agent as `UNRAID_AUTO_COLLECTORS` (`--auto-collectors`, `auto_collectors` in `config.yml`). **Disabled** keeps a collector off even when present. The disabled-collector list and runtime REST/MCP control opt a collector out; `/api/v1/collectors/status` reports `hardware_gated` and `auto`. The settings page shows the detection result and last check time, adds a **Re-check hardware** button and a persisted **Show all collectors** toggle (`SHOW_ALL_COLLECTORS`) that only reveals rows. If detection is unavailable the page shows every row. Playwright UI tests for the settings page now run in CI. ([#261](https://github.com/tinkerologie/unraid-management-agent/issues/261)) - **ZFS pool member drives with SMART health** — each pool in `/api/v1/zfs/pools` (and the `zfs_pools_update` event) now has a `drives` list: every leaf member with the whole disk it lives on (resolved through sysfs, including device-mapper and by-id names), its vdev and state, and a `health` summary from `smartctl -j -a -n standby`: model, protocol, media type, SMART status, temperature, power-on hours, NVMe percentage used / available spare / critical warning, and media errors (NVMe media errors, SCSI uncorrected errors or ATA attribute 187). smartctl gets an explicit device type from sysfs (`nvme`, `sat` or `scsi`) so it does not probe the drive, and USB-attached disks are not read. Drives are never woken: a disk Unraid reports spun down (`disks.ini`) is not read and keeps its last reading, flagged `standby`; ATA drives use a fail-closed power check (`-n standby,2,3`, so a drive whose power mode can't be read is not read either); and rotational SCSI drives, (or with an unknown rotational flag), whose power check can't fail closed, are read only when Unraid manages them and reports them spinning. The spin state is checked again right before each smartctl run. Readings are cached per disk for 5 minutes and dropped when the kernel's `diskseq` shows a different disk under the same name. At most 4 smartctl processes exist at once (a timed-out one keeps its slot until it exits; 15 s timeout). Serial numbers are never reported. ([#227](https://github.com/tinkerologie/unraid-management-agent/issues/227)) - **Fan channel labels** — each fan in `/api/v1/system` (`fans[]`) now has an optional `label`: the channel's lm-sensors label (the driver's `fanN_label` or a `sensors.conf` `label` override, e.g. `"Coolant Flow (rpm)"`), so consumers can show the user's names and tell a flow meter on a fan header from a fan. Omitted for unlabelled channels and the sysfs fallback. ([#228](https://github.com/tinkerologie/unraid-management-agent/issues/228)) - **WAN IP lookup setting** — `WAN_IP_LOOKUP` (`--wan-ip-lookup`, `wan_ip_lookup` in YAML, and a **WAN IP Lookup** toggle on the plugin settings page) controls whether requesting network access URLs asks `api.ipify.org`, `ifconfig.me` and `icanhazip.com` for the public IP. It defaults to on, so nothing changes for existing installs; when off, no external request is made and the WAN entry uses only the `WANIP=` value from Unraid's `network.cfg`. WAN discovery is now an injectable `NetworkAccessDiscovery`, with tests for service order, first-answer short-circuit, invalid answers, the `network.cfg` fallback, the disabled path and the request-only boundary; the existing access-URL tests no longer contact the real services. Follow-up to the Community Applications review (#239). ([#252](https://github.com/tinkerologie/unraid-management-agent/issues/252)). - **Network cards: NIC temperatures and optic diagnostics** — new optional `nic` collector (default every 60 s, `INTERVAL_NIC`, listed on the plugin settings page), REST endpoint `GET /api/v1/network/cards`, WebSocket event `nic_update` and read-only MCP tool `get_network_cards`. Reports one entry per PCI network card (ports grouped by PCI slot; virtual interfaces, USB NICs, SR-IOV VFs and switchdev representors skipped) with vendor, model, driver, firmware and hwmon temperatures (ASIC, PHY, MAC, module cages), and per port the link state plus the plugged optic's identity and DDM decoded from SFF-8472 (SFP, incl. external calibration) or SFF-8636/SFF-8436 (QSFP): module temperature and voltage, per-lane laser bias and TX/RX power (mW and dBm), alarm and warning flags and thresholds. Module EEPROMs are read with the `SIOCETHTOOL` `ETHTOOL_GMODULEINFO`/`ETHTOOL_GMODULEEEPROM` ioctl, so no process is spawned per port and cycle. Vendor serial numbers are never reported. ([#225](https://github.com/tinkerologie/unraid-management-agent/issues/225)) - **Every NUT device, not only the first** — `GET /nut` and `nut_status_update` have a new `statuses` list with the detailed status of every device `upsc -l` lists (a second UPS, an ATS, a PDU …), in the same order. `status` stays the first device's, and `GET /ups` stays the first NUT device (or apcupsd) and now says which one in `device_name`. A device whose query fails is logged and left out. The NUT collector's command runner, `upsc` lookup and file checks are injectable for tests. ([#206](https://github.com/tinkerologie/unraid-management-agent/issues/206)). - **Exclusive share state** — `GET /shares` (and the share WebSocket/MQTT payloads) now report Unraid 6.12+ exclusive share status per share: `exclusive` (`/mnt/user/` is a symlink that bypasses FUSE), `exclusive_eligible` (exclusive shares are enabled, the share is pool-only, and on Unraid before 7.2 it is not NFS-exported), `exclusive_pool` (the share's pool) and, when an eligible share has fallen back to FUSE, `locations` listing every pool or array disk that holds a top-level folder for it, so the stray copy can be found (`locations_unchecked` names disks and pools that were skipped, and never woken, because they are spun down, their spin state is unknown, or the check failed). `exclusive` comes from emhttp's `shares.ini`, so nothing is read through FUSE; the fields are omitted on older releases and while the array is stopped. - **Stable `device_id` for USB HID hwmon chips, and same-model fans kept apart** — `temperatures[]` and `fans[]` entries of `GET /api/v1/system` have a new `device_id` for chips on a USB HID device (fan/pump controllers such as Aquacomputer Octo/Quadro, Corsair Commander): the USB serial number, or `usb--` when the device has none. lm-sensors names these chips `-hid--`, and the HID id changes whenever the device re-enumerates, so the chip name cannot be used as an identity. It is read from `/sys/class/hwmon` once per collection and is omitted for every other chip. `fans[]` entries also report their lm-sensors chip in `source`. Fans keep their `_` names, except when two or more chips of the same model report fans (two Octos): their fans are then named `-_` (or `_` without a device ID) instead of silently overwriting each other. - **Mandatory pre-submission governance for pull requests** — added a required "Pre-Submission Governance" section to the PR template with mandatory confirmation checkboxes (built and ran locally, `make test` passes, `make pre-commit-run` passes, real verification output pasted, not a duplicate or back-to-back spam PR, and human sign-off for AI-assisted PRs). The PR Governance workflow now fails any PR that leaves these boxes unchecked, and `CONTRIBUTING.md` documents the enforced rules. These apply to all authors, including AI agents and automation. - **CI runner spam protection** — added a `concurrency` group with `cancel-in-progress` to the PR Governance workflow and gated the Coverage workflow to skip draft PRs, so rapid back-to-back pushes cancel superseded runs instead of piling up and exhausting CI runners. - **Enforced GitHub issue forms for triage quality** — replaced legacy markdown issue templates with structured GitHub Issue Forms (`01-bug-report.yml` and `02-enhancement-request.yml`) based on the `vault` repository setup, with required fields and required confirmations to read `CONTRIBUTING.md` and `AGENTS.md`. Updated `.github/ISSUE_TEMPLATE/config.yml` contact links for Discussions, private security disclosure, and contribution rules. - **Automated PR governance checks** — added GitHub Actions workflow enforcing PR template completeness, exempting bots and draft PRs, stripping HTML comments during section validation, and validating issue references (supporting issue numbers, full URLs, and explicit "None" for self-contained changes). - **GitHub issue template configuration** — added `.github/ISSUE_TEMPLATE/config.yml` with private security disclosure contact links. - **SAS storage topology** — new optional `storage_topology` collector (default every 300 s, `INTERVAL_STORAGE_TOPOLOGY`, listed on the plugin settings page), REST endpoint `GET /api/v1/storage/topology`, WebSocket event `storage_topology_update` and read-only MCP tool `get_storage_topology`. Combines storcli (controllers, PCIe link, HBA ports and lanes, enclosures, per-drive paths/link speeds/error counters) with `sg_ses --json` (PSUs, fan RPM, temperature/voltage/current sensors, I/O module status and firmware, SAS connector cable map, slot status) and derives per-element problems, I/O module firmware mismatches and path redundancy. Read-only and sequential: storcli always runs with `J nolog`, never twice at once, with timeouts; works with sg_ses alone (plain HBAs) and is a no-op (`state: "unsupported"`) without either tool. ([#184](https://github.com/tinkerologie/unraid-management-agent/issues/184)). - **Storage topology throughput and link utilization** — `GET /api/v1/storage/topology` controllers and enclosures now carry a `throughput` object (`read_bytes_per_sec`, `write_bytes_per_sec`, `total_bytes_per_sec`, `capacity_bytes_per_sec`, `utilization_percent`, `drives`, `interval_seconds`). Rates are the sector deltas of each drive's block device in `/proc/diskstats` between two collections (kernel counters only, nothing is sent to the drives); counter resets count as zero and the object is omitted on the first collection. Capacity is the SAS payload rate of the links (8b/10b: link rate in Gbps x 100 MB/s per lane): the controller's connected phys, and for an enclosure the controller ports cabled directly to it (0, with no `utilization_percent`, for an enclosure only reached through another enclosure). ### Removed - **BREAKING: plugin update actions** — the agent no longer installs or updates other Unraid plugins. `POST /api/v1/plugins/{name}/update` and `POST /api/v1/plugins/update-all` now return 404, and the MCP tools `update_plugin` and `update_all_plugins` are no longer registered (a stored tool-policy override for either name has no effect). Apply plugin updates from Unraid's **Plugins** page. Update availability checks are unchanged: the plugin update collector, `GET /api/v1/plugins/check-updates`, `POST /api/v1/plugins/updates/refresh`, and the `check_plugin_updates` and `refresh_plugin_updates` MCP tools. Required for Community Applications approval (#239). ([#244](https://github.com/tinkerologie/unraid-management-agent/issues/244)). ### Security - **Go 1.27.2 and `golang.org/x/net` v0.60.0** — release builds read the Go version from `go.mod`, which pinned 1.27.1; built with it, the agent reached standard-library `net/http` and HTTP/2 vulnerabilities (GO-2026-6617, 6613, 6612, 6611, 6610, 6609) through its API server and its Docker and LLM clients. `go.mod` now requires Go 1.27.2, and `golang.org/x/net` is updated to v0.60.0 (GO-2026-6617, 6612, 6611, 6610, 6603). `govulncheck` reports no vulnerabilities. ([#258](https://github.com/tinkerologie/unraid-management-agent/issues/258)). - **Release bundle pinned with SHA256** — the `.plg` now carries a `sha256` entity and a `` element next to ``; Unraid's plugin manager verifies SHA256 first. Each release computes both checksums, publishes `SHA256SUM` and the SHA256 in the release notes, and writes both into the `.plg` with `scripts/update-plg-checksums.sh`, which checks their format, refuses a `.plg` whose `version` differs from the release, and asserts the written values. The rewrite now runs before the GitHub release is created, so a failure blocks publication. The checksum lines carry `pragma: allowlist secret`, so detect-secrets no longer needs a baseline entry that goes stale every release. Seeded with the SHA256 of the published v2026.09.01 bundle. Reported in the Community Applications review (#239). ([#247](https://github.com/tinkerologie/unraid-management-agent/issues/247)). - **BREAKING: no unauthenticated network writes by default** — with no `API_TOKEN`, the REST API now answers `POST`, `PUT`, `PATCH` and `DELETE` with `403 Forbidden` and a message on how to set a token, and HTTP MCP (`/mcp`, `/mcp/`) treats every write tool as read-only. Reads, WebSocket subscriptions and MCP read tools still work without a token, and STDIO MCP is unaffected. `ALLOW_UNAUTHENTICATED_WRITES` (`--allow-unauthenticated-writes`, `allow_unauthenticated_writes` in YAML) restores the old behaviour and has no effect once a token is set. **Existing plugin installs without a token** get `ALLOW_UNAUTHENTICATED_WRITES="true"` once on upgrade, so their clients keep working, and the settings page shows a warning: set a token, add it to your clients, then turn the option off. **Daemons started directly** (CLI, environment or YAML) are not migrated and lose tokenless write access until `API_TOKEN` or the opt-out is set. The settings page gains the opt-out toggle and a read-only notice, and saving tool policies without a token stores them for the next start instead of failing. Reported in the Community Applications review (#239). ([#245](https://github.com/tinkerologie/unraid-management-agent/issues/245)). - **Bundle extraction no longer changes system directory owners** — release bundles stored every entry with the GitHub Actions runner owner (uid/gid 1001), and the installer's root `tar -xzf` applied it to `/usr`, `/usr/local`, `/usr/local/emhttp` and `/usr/local/emhttp/plugins` on every install and boot. The installer now extracts with `--no-same-owner --no-overwrite-dir`, restores `root:root` (non-recursive) on those directories and the plugin directory when an earlier install changed them, and `make package` builds the archive with numeric owner `0/0` on GNU tar and bsdtar. The release workflow refuses to publish a bundle with any other owner, and a new `Bundle Ownership` CI job (`make test-packaging`) runs the `.plg` extraction command and repair loop as root against a uid-1001 archive and checks tar flag selection. Reported in #239. ([#242](https://github.com/tinkerologie/unraid-management-agent/issues/242)). ### Changed - **Fan control status defaults to 60 s instead of 5 s** (#277) — `INTERVAL_FAN_CONTROL` / `--interval-fan-control` / `intervals.fancontrol` now default to 60 s in the daemon, the plugin defaults, the start script and the settings page. The collector only reports fan RPM/PWM; at 5 s its `fan_control_update` events made WebSocket consumers such as Home Assistant refresh every 5 s. The fan curve engine still evaluates curves every `poll_interval_seconds` (5 s by default), so fan control stays as responsive as before. Installs that set the interval explicitly keep their value, and 5 s is still selectable. - **README refreshed** — rewritten around what users need first: features, requirements, Community Applications install, a quick start with token handling, security and privacy, configuration, integrations and links to the full documentation. Removed duplicated endpoint lists and response samples (they live in `docs/api/`), stale claims (Unraid 6.9+, "Community Applications coming soon", old interval defaults and plugin version), a Dev Container section for a container that no longer exists, and broken links; added hardware auto-detection, UPS, NIC optics, SAS topology, ZFS drive health, alerting, diagnostics, TLS and the Home Assistant integration. - **Hardware-dependent collectors default to Auto** — fresh installs, `default.cfg` and the start script now default `INTERVAL_NIC`, `INTERVAL_STORAGE_TOPOLOGY`, `INTERVAL_GPU`, `INTERVAL_UPS`, `INTERVAL_NUT` and `INTERVAL_ZFS` to `auto`. On the first upgrade a one-time migration (`scripts/migrate-auto-intervals`, marker `.auto-intervals-migrated`) switches each of these keys to `auto` when it is missing or still equals a previously shipped default (NIC `60`/`0`, Storage Topology `300`/`0`, GPU `60`, UPS `60`, NUT `0`, ZFS `0`/`30`); any other value is kept. As a result GPU and UPS polling moves from 60 s to 5 minutes, and ZFS, NUT, NIC and Storage Topology turn on by themselves where the hardware exists. To keep a fixed interval, choose it or **Disabled** and click Apply. Collectors whose hardware is absent are off on every install, including direct daemon runs; list a collector in `UNRAID_DISABLE_COLLECTORS` to keep it off regardless. ([#261](https://github.com/tinkerologie/unraid-management-agent/issues/261)) - **Network Cards & Optics and Storage Topology collectors off by default** — most servers have no SFP/QSFP optics or SAS HBAs/enclosures, so the `nic` and `storage_topology` collectors now default to disabled (`INTERVAL_NIC=0`, `INTERVAL_STORAGE_TOPOLOGY=0`) in the daemon, `default.cfg`, the start script and the settings page. Enable them on the settings page (60 s and 5 min recommended) or with the environment variables. Installs that already saved an interval keep it. The security and privacy notes are no longer shown on the Unraid Plugins page (the plugin README); they remain in the Community Applications listing and the project README. ([#260](https://github.com/tinkerologie/unraid-management-agent/issues/260)). - **`make pre-commit-run` passes on a clean checkout** — the storage-topology hardware fixtures are excluded from the whitespace, end-of-file, line-ending, Prettier and detect-secrets hooks and marked byte-exact in `.gitattributes` and `.editorconfig`; generated Swagger keeps its missing final newline; codespell ignores `unparseable` and `ser`; `.secrets.baseline` is refreshed with every new false positive audited; Prettier and markdownlint fixes are applied (`[Unreleased]` now has one subsection per type). The gosec hook and `make security-check` run gosec (with errcheck, which covers G104) through golangci-lint, because standalone gosec v2.29.0 cannot read Go 1.27 export data. `scripts/setup-pre-commit.sh` installs golangci-lint v2 and goimports. MQTT tests accept a 0 ms connection latency, which an immediate failure can report. ([#249](https://github.com/tinkerologie/unraid-management-agent/issues/249)). - **Disclosed security and privacy behaviour** — the Community Applications listing (`unraid-management-agent.xml`) and `README.md` now state the authentication default (reads without a token, control actions refused until one is set), the REST/MCP control scope including User Scripts, and the on-request WAN IP lookups to `api.ipify.org`, `ifconfig.me` and `icanhazip.com`. Documentation only; an opt-out for the lookups is tracked in #252. Reported in the Community Applications review (#239). ([#246](https://github.com/tinkerologie/unraid-management-agent/issues/246)). - Pull requests get path labels and at most one type label, `bug` or `enhancement`, from the shared tinkerologie PR labeler. Dependabot pull requests whose largest update is a patch or minor version merge automatically once the checks and commit statuses on their head commit have finished without a failure (a Codecov check that is still pending is not waited for; a Codecov failure blocks); majors, and pull requests with open review threads, stay for review. - Release workflows hardened (least-privilege job permissions, no expressions in shell scripts, no persisted checkout credentials, no dependency caches); scheduled workflows run on Australia/Brisbane time. - Releases push the updated PLG hash to `main` with a token from the Tinkerologie-Release GitHub App, the only actor allowed to bypass the pull-request rule on `main`, so the Unraid plugin manager and Community Applications see each new release. - Tinkerologie-Bot governance report updated to v1.4.3: an older run can no longer post a stale report after a newer run passed. Workflow formatting tidied. - Repository standardised to the tinkerologie layout (STANDARDS.md in tinkerologie/.github): workflows renamed to the standard set (CI, Validate, Release, PR Governance, ...), every action pinned to a commit SHA, shared Dependabot, `.editorconfig`, `.gitattributes` and `CODEOWNERS` files, and organization-wide labels and community files. - Tinkerologie-Bot's comment is now a readable "Pull request checklist": each problem is a numbered heading with plain-English guidance and its items listed underneath, and report updates for a pull request are serialised so an older run can never overwrite a newer one. - Contribution governance: issues without a `bug` or `enhancement` label are labeled automatically when their type can be determined (issues labeled `question`, `duplicate`, `invalid`, `wontfix`, or `no-type` are skipped), so CodeRabbit can plan them. Pull requests that conflict with the default branch are labeled, and their authors are notified in a comment. Stale issues and pull requests are labeled after 14 days without activity and closed 30 days later. PR governance now re-validates a human-authored, non-draft PR when a bot such as CodeRabbit edits its description, instead of skipping the check and reporting a pass, only maintainers with write access may use `None` instead of linking an issue, and a link to an issue that does not exist now fails the check. Codecov patch coverage must now reach 90%. - Tinkerologie-Bot now posts one comment on pull requests that fail PR governance, listing every problem to fix, and updates it once they are fixed. PR governance runs from the default branch's workflow (`pull_request_target`), so a pull request cannot change its own check. - Configured Codecov GitHub Actions workflow for tokenless coverage uploads - **Faster `/services`** — the rc status checks behind `GET /api/v1/services`, the MCP `list_services` tool and the MQTT service states now run up to 4 at a time instead of one after another (about 2.7 s per request before), each with a 15 s timeout, and keep their order. New `lib.ExecCommandOutputWithTimeout` stops a status script's child processes from holding the call past its deadline. ([#201](https://github.com/tinkerologie/unraid-management-agent/issues/201)). - **GitHub Actions Ubuntu runner policy** — set release workflow jobs to `ubuntu-latest` so CI always tracks the latest supported Ubuntu GitHub-hosted image. - **Automated Codecov reporting in CI** — added a dedicated GitHub Actions coverage workflow running `go test -covermode=atomic -coverprofile=coverage.out ./...` on `push` to `main` and `pull_request` events with concurrency cancellation, uploading to Codecov via `codecov/codecov-action@v5` (including tokenless uploads for public repository fork pull requests), and configured repository status thresholds in `.codecov.yml` with exclusions for generated Swagger docs and tests. ### Tests - **Coverage uplift toward 90% (issue #192)** — added deterministic, host-safe regression tests for previously thin areas. The structured `DiagnosticLogger` (JSONL output, correlation IDs, UTC timestamps, per-level helpers, nil-context guard, concurrent writes, close) and goroutine stack helpers now have `daemon/logger` coverage of ~85% (up from ~30%). Extended `daemon/lib` redaction tests to cover the reflection-based typed map/slice paths, JSON tag aliases, `json:"-"` omission, unexported-field skipping, input immutability and nested mixed containers. Added diagnostics tests for `parseMemInfo`, the `readLastNLines` log-tail helper (missing/empty files, exact last-N ordering, no trailing newline, redaction) and ZIP archive error paths (failing writer, unsupported JSON values, nonexistent output directory, partial-file removal and empty-log omission). ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift part 2 (issue #192)** — added further host-safe unit tests covering pure functions. `daemon/domain` rose to ~88% (from ~58%) via tests for the tool-policy store (`IsValidTool`, `GetAll`, `GetCatalog`, `CategorizeMCPTool`), the typed event bus (`Topic.TopicName`, `EventBus.SubTopics`) and file-config loading (`LoadConfigFile` happy/missing/malformed paths, `DefaultDiscoveryConfig`). Added `daemon/services/controllers` tests for the pure fan-safety PWM clamp (`ValidatePWM` boundaries and default-minimum normalization), fan-curve interpolation (`interpolateSpeed` below/above/midpoint and the duplicate lowest-boundary clamp), and the Docker/IPMI string helpers (`shortDigest`, `sanitizeFanName`). ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift part 3 (issue #192)** — extracted the `/proc/net/dev` and `disks.ini` parsers in `daemon/services/collectors` into pure, `io.Reader`-based helpers (`parseNetDevStats`, `parseDisksINIFrom`) with the existing collector methods kept as thin file-opening wrappers, then added table-driven fixture tests covering multi-record parsing, header skipping, malformed/short rows, non-numeric counters, content before the first section, and final-section capture. No collector behavior changed. ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift part 4 (issue #192)** — extracted the `nvidia-smi` CSV parsing in `daemon/services/collectors/gpu.go` into a pure `parseNvidiaGPUCSV` helper (collector keeps querying the driver version), with table-driven tests for single/multiple GPUs, short records, non-numeric and `N/A` fields, zero total memory, and MiB-to-byte conversion. Also added `daemon/lib` tests for `ExecCommandOutputWithContext` (success, pre-cancelled context, nonexistent binary). No collector behavior changed. ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift part 5 (issue #192)** — extracted the `network.cfg` and `ident.cfg` parsing in `daemon/services/collectors/config.go` into pure `io.Reader`-based helpers (`parseNetworkConfig`, `parseSystemSettings`) and made the boot config directory injectable so the `GetNetworkConfig` and `GetSystemSettings` wrappers are unit-testable. Added tests for interface section matching, bond/bridge/VLAN parsing, missing-interface and missing-file errors, and system settings fields. No collector behavior changed. ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift part 6 (issue #192)** — extracted the remaining `daemon/services/collectors/config.go` settings parsers (`docker.cfg`, `domain.cfg`, `disk.cfg`) into pure `io.Reader` helpers (`parseDockerSettings`, `parseVMSettings`, `parseDiskSettings`) routed through the injectable boot config directory, with tests covering field parsing, device/network list splitting, the disabled-default fallback when `docker.cfg`/`domain.cfg` are absent, and the required-file error for `disk.cfg`. No collector behavior changed. ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). - **Coverage uplift phases 1–5 completion (issue #192)** — completed all five phases from the test coverage plan: - _Phase 1 (Reporting Alignment)_: aligned `Makefile` `test-coverage` target (`-covermode=atomic`, filtering out `daemon/docs` and `tests`), documented line vs statement coverage in `docs/development/code-quality.md`, and gated host-dependent Docker SDK and `virt-clone` tests with `//go:build integration`. - _Phase 2 (Collectors & Device Parsing)_: extracted pure parsing functions in `ups.go` (`parseAPCOutput`, `parseNUTUpscOutput`), `nut.go` (`parseNUTConfig`, `parseNUTStatusOutput`), and `zfs.go` (`parseZPoolListOutput`, `parseZPoolStatusOutput`, `parseZFSDatasetListOutput`, `parseZFSSnapshotListOutput`), adding thorough table-driven unit tests for pool states, vdev trees, and datasets. - _Phase 3 (Controllers & Execution Seams)_: added injected execution seams and unit tests for `array.go` (start/stop, parity checks, spin modes), `service.go` (start/stop/restart), `vm.go` (`pmWakeup`, snapshot lists, clone), `guard.go` (`binaryExists`), and `fan_safety.go` (temperature probes and PWM safety bounds). - _Phase 4 (Deterministic Lifecycle & Subscriptions)_: eliminated arbitrary `time.Sleep` calls in `collector_manager_test.go` and `subscribe_events_test.go` using channel waits, bounded polling helpers, and `WSHub.ClientCount()`; refactored `Orchestrator.Run` with an isolated, tested `shutdown()` helper. - _Phase 5 (Logger & Diagnostics)_: added log capture tests in `logger_test.go`, injected bundle/archive seams with comprehensive unit tests for `handleDiagnosticsBundle` in `diagnostics_test.go`, and documented redacted ZIP vs unredacted shell diagnostics in `docs/troubleshooting/diagnostics.md` and `docs/api/diagnostics.md`. ([#192](https://github.com/tinkerologie/unraid-management-agent/issues/192)). ### Fixed - **mDNS discovery advertised Docker and virtual bridge IPs** — the agent now advertises one address on one LAN interface. It uses the bind address if set, otherwise the default route's source address, otherwise the first IPv4 of `br0`, then `bond*`, then `eth*`. Docker (`docker0`, `br-*`, `veth*`), LXC/LXD/Incus/Podman, CNI, libvirt (`virbr*`, `vnet*`), Unraid shim and VPN interfaces are never used, and announcements go out on the chosen interface only. Before, a start without a default route (common early in boot) fell back to answering on every interface with that interface's own address, so Home Assistant discovered the server once per Docker bridge (`172.17.0.1`, `172.18.0.1`, ...). If no LAN interface is up yet, the agent now retries every 30 seconds instead. ([#274](https://github.com/tinkerologie/unraid-management-agent/issues/274)) - **Agents on several servers fought over one MQTT session** — the default MQTT client ID `unraid-management-agent` is now sent as `unraid-management-agent-`, so two servers on one broker no longer disconnect each other in a loop. An empty client ID gets the same treatment; any other configured `MQTT_CLIENT_ID` is used unchanged. Single-server setups see the new client ID in broker logs and ACLs. `GET /api/v1/mqtt/status` reports the ID in use. ([#275](https://github.com/tinkerologie/unraid-management-agent/issues/275)) - **VM `host_cpu_percent` was always 0** — the VM collector now fills `host_cpu_percent` in `/api/v1/vm` (and the `vm_list_update` event): the VM's share of the whole server, its CPU-time delta over the host's logical CPU count. The count comes from libvirt `NodeGetInfo`, so CPUs isolated for VMs (`isolcpus`) do not inflate the share; `runtime.NumCPU()` is the fallback. `guest_cpu_percent` stays relative to the VM's vCPUs. Both share one guarded calculation: a CPU counter that goes backwards (domain restart) no longer reports a false 100 %, it starts a new baseline. ([#175](https://github.com/tinkerologie/unraid-management-agent/issues/175)) - **ZFS scrub and resilver times, progress and repaired bytes** — `scan_start_time`, `scan_end_time`, `scan_repaired_bytes` and `scan_progress_percent` in `/api/v1/zfs/pools` were never filled in. The `zpool status` scan line is now parsed for every OpenZFS format: completed, canceled and in-progress scrubs and resilvers (including vdev-qualified sequential rebuilds such as `resilver (draid1-0) in progress`), and paused scrubs. When zpool prints several scan lines (a scrub plus a resilver, or one rebuild per vdev), an active scan wins over a paused one, and that over a finished or canceled one; between finished scans the latest end time wins. The end time comes from the timestamp (local time) and the start time from the end time minus the printed duration (or the "since" / "scrub started on" time); progress and repaired bytes come from the indented lines of an in-progress or paused scan. `scan_status` now uses the documented values (`scrub in progress`, `resilver completed`, `scrub canceled`, `scrub paused`, ...): a completed resilver was reported as in progress, and an in-progress scrub as `in progress`. ([#185](https://github.com/tinkerologie/unraid-management-agent/issues/185)). - **`config.yml` overrode command-line flags and environment variables** — `applyFileConfig` set every field the YAML file contains after flags and env were parsed, so for example `read_only: false` in `config.yml` overrode `READ_ONLY=true`. Settings given on the command line or through a non-empty environment variable now take precedence, as documented: CLI flag > env var > config file > default. An empty environment variable does not count, so the plugin start script's empty `API_TOKEN=""` no longer discards a token from `config.yml`, and an empty boolean such as `READ_ONLY=""` no longer stops the agent from starting. On plugin installs, values from `config.cfg` (exported as environment variables) now win over a `config.yml`. ([#257](https://github.com/tinkerologie/unraid-management-agent/issues/257)). - **Implausible fan readings reported as real** — a floating or noisy tach line on a Super I/O header (e.g. `nct6793_fan5` at 56784 RPM) was passed through to `GET /api/v1/system`, MCP, Prometheus, MQTT and Home Assistant. Fan readings above `lib.MaxPlausibleRPM` (25000, the limit fan control already uses), negative or not finite are now left out, on both the `sensors -u` and sysfs paths, and each dropped channel is logged once at debug level. The sysfs fallback now reads under the collector's configurable sysfs root. ([#262](https://github.com/tinkerologie/unraid-management-agent/issues/262)). - **UPS/NUT readings the UPS does not report were sent as 0** — numeric fields in `UPSStatus` (`/ups`, `ups_status_update`) and `NUTStatus` (`/nut`, `nut_status_update`) are now nullable and serialised as `null` when apcupsd/NUT does not report the variable (many UPSes have no `ups.load` or `ups.realpower`). Power is estimated from nominal × load only when both are reported, and the UPS collector's NUT path now uses `ups.realpower` when present and no longer takes `ups.power.nominal` (VA) as the watt rating. MQTT discovery templates publish `None` (unknown) instead of 0, Prometheus leaves out UPS gauges for unreported readings, and the alert variables `UPSBatteryCharge`, `UPSLoadPercent`, `UPSRuntimeLeft`, `NUTBatteryCharge`, `NUTBatteryRuntime` and `NUTLoadPercent` are nil when unknown, so rules never fire on a fabricated 0 (guard them, e.g. `UPSLoadPercent != nil && UPSLoadPercent > 80`). A rule that fails to evaluate keeps its state and is logged once until it evaluates again, instead of on every cycle. ([#204](https://github.com/tinkerologie/unraid-management-agent/issues/204)). - **FTP status and actions on Unraid 7** — Unraid starts vsftpd from inetd, but `/services` checked a non-existent `/etc/rc.d/rc.proftpd` (always stopped, actions failed with an opaque 500) and `/settings/network-services` read `ftp.enabled` from leftover Tips and Tweaks keys. FTP is now enabled when `/etc/inetd.conf` has an active `ftp` line and running when TCP port 21 is listening, as the webGUI reports it. FTP start/stop/restart return a clear "not supported, use Settings > FTP Server" error (HTTP 400). ([#199](https://github.com/tinkerologie/unraid-management-agent/issues/199)). - **CI coverage test on Linux** — allowed HTTP 200 in `TestLogFileEndpoint` when `/var/log/syslog` exists on Linux runners, unblocking coverage profile generation. - **Codecov PR comments** — configured `.codecov.yml` with `require_changes: false` so coverage reports are posted on all pull requests. - **WireGuard always reported as not running** — `/etc/rc.d/rc.wireguard status` exits 1 even when tunnels are up, so `GET /api/v1/services`, the MCP `get_service_status`/`list_services` tools and the MQTT WireGuard service switch always showed it stopped. The status check now reads the script's `Active tunnels:` line instead of its exit code. ([#188](https://github.com/tinkerologie/unraid-management-agent/issues/188)). - **Sensors stderr contamination in fan/temperature parsing** — system collector now reads only `sensors -u` stdout via `ExecCommandStdout`, preventing lm-sensors stderr warnings from corrupting parsed values and restoring missing/garbled fan entries. ([#173](https://github.com/tinkerologie/unraid-management-agent/pull/173)). - **VM disk I/O counters from single-quoted libvirt XML** — disk target extraction now accepts both single and double quotes and only scans `` elements, restoring `disk_read_bytes`/`disk_write_bytes` collection for regular vdisk-backed VMs. ([#174](https://github.com/tinkerologie/unraid-management-agent/pull/174)). - **Plugin UI API token controls** — Generate/Clear now dispatch change events so Apply enables correctly, and Show/Copy are disabled with guidance when a saved token is intentionally not rendered back into the form. ([#176](https://github.com/tinkerologie/unraid-management-agent/pull/176)). - **Temperature metric filtering from lm-sensors** — `parseSensorsOutput` now reports only `temp*_input` channels, excluding voltage/current/power/fan `_input` metrics that were previously misreported as Celsius temperatures. ([#177](https://github.com/tinkerologie/unraid-management-agent/pull/177)). - **False "Array not started" findings** ([#180](https://github.com/tinkerologie/unraid-management-agent/issues/180)) — the health report and the MCP `find_root_cause` tool compared the array state with "Started", but Unraid reports `mdState` as "STARTED", so a running array was always flagged critical. Added a case-insensitive `ArrayStatus.IsStarted()`, used by the health report, `find_root_cause` and the Prometheus metrics. - **Disk spin up/down REST endpoints** ([#178](https://github.com/tinkerologie/unraid-management-agent/issues/178)) — added `POST /api/v1/disks/{id}/spinup` and `POST /api/v1/disks/{id}/spindown`, which the Home Assistant integration's disk spin switches already call (they got 404). `{id}` may be the disk id, device or name; it is resolved against the disk collector cache, so unknown disks return 404, and the existing array controller spins the disk via emhttpd. - **Plugin update check and plugin updates** — the hourly check and `POST /plugins/updates/refresh` now run `plugin checkall` (the previous `plugin check` without a plugin file only printed usage and exited 1, so no update metadata was downloaded); the `plugin_update` collector publishes its first result even when no plugin has an update and stamps results with the check time, so `GET /plugins/check-updates` no longer stays empty with a zero timestamp; and `POST /plugins/{name}/update` accepts the name `GET /plugins` reports when it differs from the `.plg` file name (e.g. `disklocation` → `disklocation-master.plg`). ([#182](https://github.com/tinkerologie/unraid-management-agent/issues/182)). - **ZFS corrupted files never reported** — `parsePoolStatus` ended the `zpool status -v` errors section at the blank line that OpenZFS prints after "Permanent errors have been detected in the following files:", so `corrupted_files` was always empty and the alerting `ZFSCorruptedFiles` total and MQTT corrupted files sensor were always 0. It now collects every 8-space-indented path after the header. `corrupted_files` is also always present (no longer `omitempty`): `[]` for a pool without permanent errors, so clients can tell "none" from "not reported", and `null` only if `zpool status` could not be read. The MQTT sensor template counts `null` or a missing field as 0. ([#190](https://github.com/tinkerologie/unraid-management-agent/issues/190)). ## [2026.09.01] - 2026-09-28 ### Added - **Zero-config MCP onboarding** — added path-based Connect URL authentication (`/mcp/` via `--mcp-connect-secret` / `MCP_CONNECT_SECRET` / `mcp_connect_secret` in `config.yml`, validated as 32–256 chars of `[A-Za-z0-9_-]`) using constant-time SHA-256 digest comparison and automatic `/mcp/` log masking; auto-generated a `private_` secret on plugin startup (`scripts/start`) when not explicitly disabled; extended `_unraid-agent._tcp` mDNS TXT announcements with `mcp_path=/mcp`, `mcp_transport=streamable-http`, and `mcp_auth=bearer|none`; added `mcp_connection_events_total` Prometheus metrics and rate-limited handshake diagnostics; and displayed `Connect URL(s):` (`http(s)://:/mcp/private_ (direct access)` and `http(s)://:/mcp`) directly in the Unraid WebGUI settings page. ([#165](https://github.com/tinkerologie/unraid-management-agent/issues/165)). ### Fixed - **Streamable HTTP `GET /mcp` hanging indefinitely** — implemented `Flush()` (`http.Flusher`) and `Unwrap()` on `statusRecorder` in `loggingMiddleware` so `http.NewResponseController(w).Flush()` inside the MCP Go SDK reaches the underlying HTTP response writer, returned an immediate framed `405 Method Not Allowed` (`Allow: POST, DELETE`, `Content-Length`) for sessionless `GET /mcp` probes instead of stalling until server timeouts, and cleared per-request read/write deadlines via `http.ResponseController` on session-bound `GET /mcp` SSE streams ([#166](https://github.com/tinkerologie/unraid-management-agent/issues/166)). ## [2026.09.00] - 2026-09-23 ### Added - **Per-tool MCP access policy** — added granular per-tool access policies (`default`, `hidden`, `read_only`, `allow`, `ask`) to control tool visibility and execution for AI agents, complete with `GET`/`PUT` REST API endpoints at `/api/v1/mcp/tool-policy`, WebUI settings panel with search, category filtering, and bulk policy controls, and persistent storage in `/boot/config/plugins/unraid-management-agent/tool_policy.json` ([#156](https://github.com/tinkerologie/unraid-management-agent/issues/156)). - **WebUI API Token management** — added an Authentication section to the Unraid WebUI settings page with cryptographic token generation, clipboard copy, password show/hide toggle, security advisories for unauthenticated configurations, single-quoted persistence in `scripts/apply`, and authenticated server-side proxying in `include/diagnostics.php` ([#159](https://github.com/tinkerologie/unraid-management-agent/issues/159)). ### Changed - **Go 1.27.1 toolchain upgrade & dependency update** — upgraded project toolchain to Go 1.27.1, adopted Go 1.27 language features (generic methods on `EventBus.Publish[T]`, native standard-library `uuid` package replacing `github.com/google/uuid`, iterator-based `strings.SplitSeq`, `slices.Contains`, embedded struct literal field selectors, and `new(...)` pointer literals), and updated all project dependencies (`modelcontextprotocol/go-sdk` v1.8.0, `moby/client` v0.6.0, `moby/api` v1.56.0, `shoutrrr` v0.21.1, and `golang.org/x/*`). ### Fixed - **Start script character sanitization escaping** — corrected trailing backslash escaping in `meta/plugin/scripts/start` (``tr -d "'\"`\$\\";"``), eliminating the `tr: warning: an unescaped backslash at end of string is not portable` warning on Unraid Linux systems ([#155](https://github.com/tinkerologie/unraid-management-agent/issues/155)). - **Unraid OS update detection** — updated `OSUpdateCollector` to inspect `/tmp/unraidcheck/result.json` and dynamix unraidcheck fallback, wired `OSUpdateCache` directly into the `/api/v1/updates` REST handler, and used `isVersionNewer` semantic comparison in `SettingsCollector.GetUpdateStatus` so `os_update_available` and version details report accurately without false positives ([#162](https://github.com/tinkerologie/unraid-management-agent/issues/162)). - **Plugin settings apply script shell syntax** — fixed missing `esac` in `meta/plugin/scripts/apply` API token unquoting block. - **Confirmation gates on destructive MCP tools** — enforced required `confirm=true` safety gates on `run_runbook`, `delete_vm_snapshot`, `remote_share_action`, `restore_fan_defaults`, and fan speed/mode/profile modification tools, bringing implementation into exact alignment with tool documentation ([#158](https://github.com/tinkerologie/unraid-management-agent/issues/158)). - **Remote share mount stall & Home Assistant state sync** — cached `statfs` capacity calls with background probing so slow or unresponsive remote CIFS/NFS shares never stall mount status reporting, triggered immediate MQTT state publication following mount/unmount operations, and bounded MQTT client publish waits with a 5-second timeout ([#148](https://github.com/tinkerologie/unraid-management-agent/issues/148)). ## [2026.08.03] - 2026-08-26 ### Fixed - **Plugin update CLI argument format** — normalised plugin names in `PluginController.UpdatePlugin` to pass bare `.plg` filenames instead of absolute filesystem paths to the Unraid `plugin update` CLI command, which rejects arguments containing path separators, added input validation to the MCP `update_plugin` tool, and introduced an injectable command execution seam on `PluginController` with comprehensive regression test coverage ([#153](https://github.com/tinkerologie/unraid-management-agent/issues/153)). - **Developer tooling and pre-commit checks** — added gosec G115 annotation for `syscall.Statfs_t` block size conversion in `unassigned.go` so `golangci-lint run` passes cleanly on Linux targets, regenerated `.secrets.baseline` with active plugins to restore `detect-secrets` scanning, and pinned `swag` CLI to `v1.16.6` in `Makefile` and CI release workflow to preserve Swagger 2.0 format reproducibility ([#150](https://github.com/tinkerologie/unraid-management-agent/issues/150)). - **Test safety against accidental machine reboot or shutdown** — made `SystemController` command execution injectable with safe defaults and wired `SystemControllerInterface` into API and MCP servers with test mocks, preventing test runs on systemd Linux hosts from inadvertently issuing destructive `/sbin/shutdown` commands during `make test` ([#149](https://github.com/tinkerologie/unraid-management-agent/issues/149)). ## [2026.08.02] - 2026-08-24 ### Added - **Optional bearer-token authentication** — a new `API_TOKEN` setting (`--api-token` flag, `API_TOKEN` env var, `api_token` config key) makes the agent require `Authorization: Bearer ` on every request. It protects all REST endpoints, `/mcp`, and `/metrics`; `/api/v1/health` stays open so external uptime monitoring keeps working without a credential. The token defaults to empty, which leaves the API unauthenticated exactly as before, so upgrades are non-breaking. Comparison is constant-time via `crypto/subtle`, authentication runs after rate limiting so unauthenticated floods are throttled first, CORS preflights are unaffected, and rejected requests get `401` with a `WWW-Authenticate: Bearer` challenge. The token is redacted from diagnostics output automatically. ### Changed - **MCP snapshot tools collapsed behind one registration helper** — the ~24 read-only monitoring tools that simply return a cached snapshot (system, array, disks, shares, GPU, hardware, ZFS, settings, etc.) now register through a single generic `addReadTool` helper instead of ~12 lines of duplicated boilerplate each. The helper concentrates the `ReadOnlyHint` annotation, availability check, and JSON encoding in one place; each tool is now a one-line spec (name, description, empty message, getter). Tool names, descriptions, input schemas, and responses are unchanged. - **REST and metrics reads routed through the cache interface** — the REST GET handlers and the Prometheus metrics exporter now read cached data through the `CacheStore` getter methods (the same interface the MCP server and alerting engine already use) instead of reaching into raw atomic cache fields. This closes the seam so cache-shape changes stay local to the store; responses are unchanged. ## [2026.08.01] - 2026-08-10 ### Security - **Dependency vulnerability remediation** — updated `go.opentelemetry.io/otel` 1.43.0 → 1.45.0 to clear GO-2026-5158 (unbounded baggage header parsing). `govulncheck` reports zero vulnerabilities in imported packages; no affected code paths were called. - **LLM client SSRF review** — annotated the OpenAI/Anthropic provider requests with justified `#nosec G704` markers (the request endpoint is an admin-configured API URL, not untrusted input). `gosec` and `golangci-lint` now report zero issues. - **Agent memory store directory hardening** — the store directory is created at `0750`, and on every save any pre-existing directory left world-readable (e.g. `0755` from an earlier install) is tightened to at most `0750` (group-write and all "other" bits cleared, stricter permissions preserved). ### Changed - **Toolchain** — bumped the `go` directive to Go 1.26.5; CI now derives its Go version from `go.mod` via `setup-go`'s `go-version-file` to prevent drift. - **Dependencies** — refreshed direct and transitive modules to their latest releases (`kong`, `go-libvirt`, `docker/go-connections`, the `go-openapi`/`swaggo` stack, and the OpenTelemetry instrumentation packages) via `go get -u ./...` + `go mod tidy`. ### Documentation - **MCP transport** — clarified that the official Go SDK negotiates the highest mutually-supported protocol version per connection, and documented the opt-in `StreamableHTTPOptions.Stateless = true` (sessionless 2026-07-28) transport, left disabled so existing session-based clients are unaffected. ## [2026.08.00] - 2026-08-10 ### Fixed - **Info-severity alerts now reach the `unraid` channel** (#144) — the alert dispatcher sends importance `normal` for info-severity events, which `CreateNotification` rejected, so those dispatches always failed with `invalid importance level: normal` before any file was written. `CreateNotification` now accepts `normal` — the stock notify script's own level for informational notifications — alongside `alert`, `warning`, and the agent's own `info`. - **`list_shares` now reports `nfs_export` correctly** (#141) — the share collector read the SMB export flag for the NFS field, so every share reported `nfs_export: false` regardless of configuration. NFS export is now derived from the dedicated `shareExportNFS` flag, and SMB/NFS export flags are parsed from the authoritative `"e"`/`"-"` values. - **`disk_spin_up`/`disk_spin_down` restored on Unraid 7.3.x** (#140) — direct writes to `/proc/mdcmd` were rejected with `invalid argument`. Spin commands now go through the emhttpd socket (matching the Unraid WebGUI), falling back to `/proc/mdcmd` when the socket is unavailable, with disk names validated against whitespace/newline injection. - **Disk collector no longer stacks blocked collect goroutines when runs overlap** (#138) — overlapping trigger paths (ticker/fsnotify/watchdog wrapper) now skip a new disk collect cycle if the previous one is still running, instead of blocking on the collector mutex and repeatedly emitting long `sync.Mutex.Lock` stall stack traces into the agent log. - **FileWatcher callback in-flight guard** (#138) — the debounced fsnotify callback now skips a new firing if the previous callback is still executing, preventing additional goroutines from stacking up behind the collector mutex. - **Watchdog stall-dump throttling** (#138) — full goroutine stack dumps are now emitted at most once per 5-minute cooldown window per collector, preventing a persistently slow collector from flooding `/var/log` with repeated multi-megabyte dumps. - **Goroutine stack dump size capped at 512 KiB** (#138) — `AllGoroutineStacks()` now truncates output so a single log entry cannot exceed the lumberjack rotation threshold. - **`ExecCommandWithTimeout` no longer blocks past deadline on stuck processes** (#138) — stdout reading now runs on a separate goroutine and unblocks when the context expires, so a `smartctl` call against an unresponsive disk cannot hold the calling goroutine indefinitely past its configured timeout. ### Changed - **Dependencies** — bumped `github.com/nicholas-fedor/shoutrrr` from 0.16.3 to 0.17.0 (#146). ## [2026.07.00] - 2026-07-10 ### Fixed - **Agent-created notifications no longer crash the stock notification parser** (#134) — `CreateNotification` now writes `.notify` files in a stock-compatible format matching the webGui `notify` script's output: an unquoted unix-epoch `timestamp` as the first line (the stock PHP parser blindly `date()`-formats the first value, so the old `event="..."` first line caused a fatal TypeError that killed every notification poll), stock field order and value escaping, a stock-style `safe_filename()` file name (`Event_Name_.notify`), and an archive copy written first (archiving now removes the unread file and keeps that copy, like the stock `archive` verb). Files are written atomically (temp file + rename, cleaned up on failure), so a full disk can no longer leave 0-byte `.notify` files that the UI flags as invalid notifications. The notification collector now parses stock-format files (unquoted epoch timestamps, escaped values) while still reading the legacy quoted-datetime format. ## [2026.06.10] - 2026-06-26 ### Added - **Native HTTPS/TLS for the HTTP & MCP server** (#131) — the agent can now serve the REST API and the `/mcp` endpoint over HTTPS when pointed at a PEM certificate/key pair via `--tls-cert-file`/`--tls-key-file`, the `TLS_CERT_FILE`/`TLS_KEY_FILE` environment variables, or `tls_cert_file`/ `tls_key_file` in `config.yml`. TLS is enabled only when both files are set; an invalid, half-configured, or unloadable pair logs a warning and falls back to plain HTTP so a stale path can never make the agent unreachable. mDNS discovery now advertises a `scheme=https`/`scheme=http` TXT record and the `/network/access-urls` endpoint returns `https://…` URLs when TLS is active. ### Fixed - **Claude Desktop "HTTP is not supported" when adding the agent** (#131) — the Claude integration guide now documents that Custom Connectors are reached from Anthropic's cloud (so a LAN URL is unreachable even over HTTPS) and gives the two paths that actually work: the `mcp-remote --allow-http` stdio bridge for LAN-only use (no TLS required), and native HTTPS with a publicly-trusted certificate behind a port-forward/tunnel for Custom Connectors. ## [2026.06.09] - 2026-06-16 ### Fixed - **The agent no longer fights third-party fan-control plugins** (#128) — fan state is restored on shutdown only for fans the agent actually modified. Previously `RestoreAll()` ran on every daemon shutdown (including plugin auto-updates) and wrote `pwm_enable` to **every** controllable fan even when the user never enabled the agent's fan control, yanking the enable-mode out from under controllers like FanCTRL Plus and Dynamix Auto Fan Control. The agent now tracks which fans it wrote to and leaves all others untouched, so a monitor-only install performs zero fan writes. ### Added - **Automatic stand-down when a third-party fan controller is active** (#128) — the agent now detects **FanCTRL Plus** and **Dynamix Auto Fan Control** (each counted active only when installed _and_ enabled, via its `service="1"` config or a running control process). While one is active the agent stays monitor-only: `SetSpeed`, `SetMode`, `SetProfile`, `RestoreDefaults` refuse with a clear "deferred to active plugin" error and the emergency full-speed override is suppressed (the active plugin owns thermal response). Detection is evaluated live, so enabling a plugin after startup takes effect without a restart. The fan status (`GET /api/v1/fan`, MQTT, dashboard feed) now includes an `external_control` object reporting whether the agent is deferring and to which controller. ## [2026.06.08] - 2026-06-15 ### Added - **Unassigned Devices Root Shares are now reported** (ha-unraid-management-agent#83) — the agent previously skipped UD "Root Share" entries. It now surfaces them in `GET /api/v1/unassigned` with `type: "root"`, covering both configured `protocol="ROOT"` entries in `samba_mount.cfg` (e.g. `//server/mnt/user`) and live local root shares gathered under `/mnt/rootshare/` (a `fuse.shfs` mount). - **Per-share status debug logging for Unassigned remote shares** — at debug level the collector now logs each remote share's `source`, `type`, `status`, and `mount_point` every cycle, so a "HA shows mounted but Unraid shows unmounted" report can be checked against exactly what the agent served (ha-unraid-management-agent#83). - **Diagnostics tools in the plugin UI** — the **Diagnostics** section now has a **Download Diagnostics** button and a **Run Self-Test** button: - **Download Diagnostics** saves a redacted ZIP (system state, array, containers, VMs, network, recent agent & system logs, and configuration with secrets removed) to attach to bug reports. It is served by a new `GET /api/v1/diagnostics/bundle` endpoint — the same bundle the CLI `diagnostics` command produces, now downloadable from the browser. The download streams through a same-origin PHP proxy so it works in every browser (Safari blocks cross-origin downloads) and on HTTPS webGUIs. - **Run Self-Test** calls `GET /api/v1/diagnostics/self-test` (via the same proxy, since the agent does not send CORS headers) and shows the Unraid version, overall data-source health, capability count, and per-subsystem status inline. - **Debug logging toggle in the plugin UI** — a new **Diagnostics** section on the settings page with a **Debug Logging** switch, so verbose logging can be enabled from the web UI instead of editing `config.cfg` over SSH. Off keeps the level at `info`; on sets `debug`. A manually-set `warning`/`error` level is preserved unless the toggle is touched. Applying restarts the service. - **Collector stall diagnostics (watchdog) across the I/O-bound collectors** (investigating ha-unraid-management-agent#83, #123) — each collect cycle now logs its start and duration at debug level, and a watchdog logs a warning plus a full goroutine stack dump if a cycle runs longer than an **interval-derived threshold** (the collector's interval, clamped to 30s–5min), then logs again when it eventually finishes. Collectors run cycles serially on one goroutine, so a single blocked cycle stops that collector's updates until it returns; this makes the blocking call visible in the agent log. Wired into the collectors that shell out or do blocking I/O — unassigned, disk, docker, zfs, ups, nut, network, shares, gpu, hardware, mover, and array — covering both startup and periodic cycles. The watchdog is purely observational; it never cancels or alters a cycle. Set the plugin log level to **debug** to capture this. - **WebSocket slow-client eviction is now logged** — when a client's send buffer fills (it cannot keep up with the broadcast rate, e.g. on a congested link) the hub evicts it so other clients are unaffected; this previously happened silently and surfaced to the consumer only as a dropped connection ("EOF"). The eviction now logs a warning naming the client and topic, so the disconnect is diagnosable from the agent log. ### Changed - **Left-aligned the plugin settings layout** — the form labels and inputs were centered (a wide right-aligned label column); they are now left-aligned in a compact column so the settings read naturally from the left. - **Redesigned plugin icons** — the Community Applications icon (`icon.png`) was a pixelated 256×256 upscale; it is now a crisp 512×512 PNG (CA requires 256×256 minimum with transparency) rendered from a 4× supersampled flat design (dark slate squircle, server slats with status LEDs, Unraid-orange telemetry arcs). The webGUI page icons (48/64/128 px under `meta/plugin/images/`) are exported from the same master, and `scripts/generate_icon.py` regenerates all sizes from one source. ### Fixed - **Bounded the Unassigned Devices statfs probe goroutines to prevent an unbounded leak** — capacity probes run in a goroutine because `statfs` on a dead network mount cannot be cancelled. Previously each cycle could leak one such goroutine per wedged share, so a permanently-unreachable remote server would grow the abandoned-goroutine (and pinned-OS-thread) count without bound until the daemon ran out of memory. Probes are now capped (16 in flight); once that many are stalled, new probes are skipped (the share is reported without capacity) instead of spawning more, so memory stays bounded. Also fixed a data race in the statfs timeout test so the full suite is race-clean. - **WebGUI footer stuck on "unraid-management-agent started on port …" after array start** (#123 follow-up, reported in ha-unraid-management-agent#83) — the plugin's `started`/`stopping_svcs` event scripts wrote their status line to stdout, which emhttpd captures into its `fsProgress` variable; the webGUI footer displays `fsProgress` until the next array operation overwrites it, so on systems that stay idle after boot the text lingered for many hours and made the agent look hung even when it was healthy (it remained even with the service stopped). Event scripts now route their output to syslog via `logger -t unraid-management-agent` instead of stdout, so the footer is never touched and the start/stop trace is still logged. ## [2026.06.06] - 2026-06-12 ### Added - **MCP read-only mode** (#126) — a new `READ_ONLY` setting (plugin settings page under **AI Agent Access (MCP)**, `READ_ONLY="true"` in `config.cfg`, `read_only: true` in `config.yml`, or `--read-only` / `READ_ONLY=true`) that blocks **every state-changing MCP tool** (49 of the 126 registered tools) at the server, so AI agents can only consume data. Write tools stay visible in tool listings but every invocation is rejected with a clear message, and each blocked attempt is logged with the tool name. The dual-mode diagnostic tools `system_health_report` and `run_runbook` still return their report / dry-run plan but never execute remediation actions, even with `confirm: true`. Destructive tools keep their existing `confirm: true` gating when read-only mode is off; the REST API and WebSocket are not affected. - **Configurable HTTP bind address** (#125) — a new `BIND_ADDRESS` setting (plugin settings page, `BIND_ADDRESS="…"` in `config.cfg`, `bind_address: …` in `config.yml`, or `--bind-address` / `BIND_ADDRESS=…`) that binds the API/WebSocket/MCP server to a specific IP on multi-homed / multi-VLAN systems instead of all interfaces. The settings page offers a dropdown of the IPs detected on the server's interfaces and VLANs (container/VM bridges and link-local excluded), so no manual entry is needed. mDNS discovery advertises the configured address, so Home Assistant auto-discovers the endpoint on the intended network. The address must be assigned to a local interface, and loopback addresses (127.x.x.x, ::1) are rejected so integrations can always reach the agent — invalid or stale values (e.g. after a VLAN change) log a warning and fall back to all interfaces rather than leaving the agent unreachable. Applying the setting from the plugin page validates it server-side, persists it, and restarts the service automatically, then health-probes the new endpoint to confirm the agent came back up. IPv6 addresses are supported. Defaults to all interfaces (unchanged behaviour). --- ## [2026.06.05] - 2026-06-11 ### Fixed - **Parity schedule endpoint missing `month` and `cron` from dynamix.cfg** (#124) — `GET /api/v1/array/parity-check/schedule` did not parse two keys from the `[parity]` section, so consumers (e.g. the Home Assistant integration) could not compute the next check for **yearly** schedules (missing `month`, fell back to January by coincidence) or **custom** schedules (missing raw `cron` expression — next check showed "Unknown"). Both are now parsed and exposed (`month`, `cron`). Additionally, the authoritative scheduled-check entry from `parity-check.cron` (the line Unraid actually runs, e.g. `0 0 1 1 *`) is exposed as **`check_cron`**, valid for every scheduled mode including custom, so consumers can compute the exact next check without re-implementing Unraid's scheduling rules. The DTO now also documents Unraid's conventions: `day` is cron day-of-week (0 = Sunday) and the dynamix `hour` key holds a `"minute hour"` pair (only the hour part is exposed — use `check_cron` for the exact time). - **Agent stalled for 1+ hour after array start on networks without outbound internet access** (#123) — several collector and API paths performed blocking network operations with long (or no) timeouts, so on isolated VLANs (no internet, unreachable remote-share servers) the agent kept serving stale data long after boot: - **Unassigned Devices remote shares**: the collector ran `statfs()` sequentially on every mounted SMB/NFS share with no timeout. A share whose server is unreachable blocks `statfs` in uninterruptible sleep for minutes; with many remote shares mounted at boot this wedged the whole collector for hours. Capacity probes are now bounded at **5 s per share** — on timeout the share is still reported (without capacity data) and the collector moves on. - **Docker container update checks**: `CheckAllContainerUpdates` ran under an independent 5-minute background context, with each per-container registry check holding its own 60-second context, and could not be cancelled on shutdown. Checks are now context-aware end to end (cancelled with the collector lifecycle) and bounded at **30 s overall / 10 s per container**. - **Plugin update checks**: `plugin check` (which downloads update metadata from GitHub) ran with a 120-second timeout and could not be cancelled. It is now context-aware and bounded at **30 s**; locally cached update files are still read afterwards, so previously fetched results are preserved. - **WAN IP discovery** (`/api/v1/network/access-urls`): each of the three public-IP services was tried with a 5-second timeout (up to 15 s per request on a blackholed network). Reduced to **2 s per service** (~6 s worst case); the first successful response still wins. ### Added - **`last_healthy` timestamp per subsystem** in the platform health registry, exposed via `GET /api/v1/diagnostics/self-test` (#123). The registry stamps the timestamp on every healthy report and preserves it across degradations, so a persistent `degraded`/`unavailable` state can be dated from the API ("when did this subsystem last work?"). Omitted from JSON when a subsystem has never been healthy. Documented in Swagger as RFC3339 `date-time`. ## [2026.06.04] - 2026-06-08 ### Fixed - **API rate limit throttled the Home Assistant integration** — the REST API used a single **global** bucket (**10 req/s, burst 20**), but the HA integration fetches 20-30 endpoints in parallel on every coordinator refresh (and retries rate-limited ones, amplifying the startup burst). One client's burst could exhaust the shared bucket and return **HTTP 429** on endpoints such as `/unassigned`, so the data wasn't available when the integration created entities and the **remote-share binary sensors never appeared** (ha-unraid-management-agent#83). Rate limiting is now **per-client (keyed by source IP)** at **50 req/s, burst 100 per client**, so legitimate LAN clients (the integration, the Web UI, MCP/AI clients) are never throttled by one another — only a single client genuinely running away is capped. Idle client buckets are swept after 10 minutes to bound memory. Rejections are now logged at debug for diagnosability, since rate-limited requests short-circuit before the logging middleware. Limits are named constants (`rateLimitPerSecond`, `rateLimitBurst`) guarded by a regression test. - **Disabled Docker/VM falsely reported as a degraded data source** — when Docker (`DOCKER_ENABLED="no"`) or the VM manager (`SERVICE="disable"`) was intentionally turned off in Unraid settings, the respective collector reported the subsystem as `unavailable`, which pushed `DegradedSubsystemCount > 0` and fired the **"Agent data source degraded"** alert on every server with those services off. A new `SourceDisabled` state (severity 0, **not** counted as degraded and not worsening the overall self-test state) is now reported when a service is positively detected as disabled in `docker.cfg`/`domain.cfg`. A service that is enabled but genuinely unreachable is still reported as `unavailable`, and an unreadable config conservatively keeps the old `unavailable` behavior so real faults are never masked. The subsystem's inline `source_status` now reads `disabled` instead of `unavailable`, and the Swagger `dto.SourceState` enum gained the `disabled` value. ## [2026.06.03] - 2026-06-07 ### Added - **Control feature parity with the official Unraid API** — added the safe control operations the official GraphQL API had that this agent lacked: **VM reset** (`vm_action reset` + `POST /vm/{name}/reset`, hard power-cycle via libvirt `DomainReset`, `confirm=true` required on both REST and MCP); Docker **container remove** (`container_action remove` + `POST /docker/{id}/remove`, optional image removal, `confirm=true`), **autostart control** (`POST /docker/{id}/autostart` + `set_container_autostart`, writing Unraid's `/var/lib/docker/unraid-autostart`), and read-only **port-conflict detection** (`GET /docker/port-conflicts` + `get_port_conflicts`); and array **clear-disk-statistics** (`POST /array/clear-disk-stats` + `clear_disk_stats`, via the emhttpd socket — the same path the WebUI uses). The MCP tool count is now 125. Destructive ops are confirm-gated and capability-gated. Verified live on Unraid 7.3.1. Per-disk array mount/unmount was intentionally **dropped** — Unraid exposes no safe per-disk mechanism (only array-wide start/stop) — and the dangerous add/remove-disk-to-array operations were deliberately left to the official API / WebUI. - **OS-resilience & self-diagnostics** — the agent now detects when an Unraid data source breaks (moved path, changed file format, missing binary) via capability/shape probing, degrades gracefully (keeps serving best-effort data, never silently wrong/empty), and surfaces it everywhere: `GET /api/v1/diagnostics/self-test`, the `run_self_test` MCP tool (read-only; brings the MCP tool count to 122), an inline `source_status` flag on affected subsystem responses (omitted when healthy, so healthy responses are unchanged), a `source_status_changed` WebSocket event, `unraid_subsystem_status` / `unraid_degraded_subsystem_count` Prometheus gauges, a degraded summary in `/api/v1/health/report`, and a built-in **enabled** `subsystem_degraded` alert rule. Control operations are capability-gated (clear "unavailable" errors instead of cryptic shell failures). Implemented as a new dependency-light `daemon/platform` package; fully self-contained (no dependency on the official Unraid API). Verified live on Unraid 7.3.1 (self-test healthy across 6 subsystems; golden-fixture + breakage-simulation tests in CI). - **Go runtime & process metrics on `/metrics`** — the Prometheus endpoint now exposes the standard Go runtime and process collectors (`go_goroutines`, `go_memstats_heap_inuse_bytes` and the full `go_memstats_*` family, `process_resident_memory_bytes`, `process_open_fds`, …), enabling long-term goroutine/heap/RSS leak and resource-health monitoring of the agent itself. Verified live on Unraid 7.3.1. - **Drive-aware fan curves + sensor discovery** — a fan curve can now read its temperature from a selected set of drives (the **max** of the active ones, sourced non-destructively from `disks.ini` — spun-down drives are skipped, never woken) and **fail over** to a per-profile hwmon sensor when those drives are spun down (logged once per transition, no spam). New read-only `GET /api/v1/fans/sensors` endpoint and `get_fan_sensors` MCP tool list every hwmon temperature sensor (path, label, value, plausibility flag) and drive (id, device, temp, spin state) available as a curve source — so users no longer need to hand-discover sysfs paths. `POST /fans/profile` and `set_fan_profile` accept a structured `source` (`hwmon` single sensor, or `drives` with optional fallback) while remaining backward compatible with the legacy `temp_sensor_path`; existing `fancontrol.json` assignments migrate transparently on load. Source paths are validated to the `/sys/class/hwmon/hwmonN/tempM_input` shape and drive IDs are sanity-checked. The emergency thermal cutoff remains **hwmon-only** by design (a spun-down array can never suppress it). MCP tool count → 126. Mirrors the `SimonFair/IPMI-unRAID` drive-selection model. Verified live on Unraid 7.3.1. ### Fixed - **Fan-safety log spam** — the fan safety guard logged a "fan appears stalled" warning on **every** poll cycle for each driven-but-not-spinning header (e.g. empty/unused fan headers reading 0 RPM), producing thousands of warnings. It now logs the warning **once per transition** into the stalled state (and a recovery line once when it clears), mirroring the OS-resilience registry's transition logging. Verified on Unraid: warnings/36-min dropped from ~3,600 to single digits. - **Clean-shutdown logged at ERROR** — a graceful HTTP-server shutdown (`http.ErrServerClosed`) was logged as `ERROR: API server error`; it is now an informational "API server stopped" line. ## [2026.06.02] - 2026-06-06 ### Added - **Zeroconf/mDNS auto-discovery** (#120) — the agent now advertises itself on the local network via mDNS/DNS-SD as `_unraid-mgmt-agent._tcp.local.`, so integrations such as the [Home Assistant integration](https://github.com/tinkerologie/ha-unraid-management-agent) can auto-discover the server instead of requiring a manually entered host/port. The advertised TXT records expose the agent version, REST API base path (`/api/v1`), and server name. Enabled by default; controllable via `--discovery-enabled` / `DISCOVERY_ENABLED` and an optional instance-name override `--discovery-service-name` / `DISCOVERY_SERVICE_NAME`, plus a `discovery:` section in the YAML config file. Advertising is best-effort and coexists with Unraid's existing `avahi-daemon`. Implemented with `github.com/grandcat/zeroconf` (DNS-SD/Avahi-compatible). The agent detects the primary LAN IPv4 (default-route source address) and advertises only that reachable address, so it never publishes unreachable Docker/`virbr0` bridge IPs on multi-interface Unraid hosts. Verified live on Unraid: a separate LAN machine browsed `_unraid-mgmt-agent._tcp`, resolved the instance to `.local:8043` with the correct TXT records, and reached the advertised LAN IP. - **Agent Skill for AI coding agents** (#121) — added a portable Markdown knowledge pack under `skills/unraid-management-agent/` following the open [Agent Skills standard](https://agentskills.io), teaching agents how to monitor and control Unraid through the agent's MCP server (121 tools, 5 resources, 6 diagnostic prompts) and REST API. Installable via `npx skills add tinkerologie/unraid-management-agent`, as a Claude Code plugin (`.claude-plugin/marketplace.json`), or as a claude.ai/Claude Desktop skill zip. The skill bundles the full tool catalog (with read/write + destructive flags), connection setup for every MCP client, the diagnostic prompts/resources, the REST surface for non-MCP clients, and request → tool workflows. - **ChatGPT Custom GPT Actions** (#121) — added a curated OpenAPI 3.0 schema (`docs/integrations/chatgpt/openapi-actions.yaml`, ~30 endpoints with `operationId`s and `x-openai-isConsequential` on state-changing operations) plus a setup guide so ChatGPT can monitor/control Unraid via REST Actions. - **AI integrations index** (`docs/integrations/README.md`) and a Claude integration guide (`docs/integrations/claude/README.md`). ### Changed - Upgraded the transitive `github.com/miekg/dns` dependency from `v1.1.27` (2020) to `v1.1.72` while adding the zeroconf dependency. - Updated `docs/integrations/mcp.md` counts to the current 121 tools / 74 read-only, replaced the stale 3-prompt list with the actual 6 diagnostic prompts, and cross-linked the new Claude/ChatGPT integration guides. --- ## [2026.06.01] - 2026-06-05 ### Added - **Remote share mount/unmount control** (issue #115; resolves the mount/unmount toggle ask in tinkerologie/ha-unraid-management-agent#79) — SMB/NFS remote shares can now be mounted and unmounted by source. Added the REST endpoints `POST /api/v1/unassigned/remote-shares/mount` and `.../unmount` (body `{"source": "//server/share"}`), the MCP `remote_share_action` tool, and an MQTT Home Assistant **switch** per share. Operations are delegated to the plugin's `rc.unassigned` control script so credentials, protocol versions, and mount options match the Unraid web UI. Sources are validated (`ValidateRemoteShareSource`) and passed as discrete exec arguments (never via a shell). Verified live on Unraid: mount via REST and unmount via MCP both toggled a real CIFS mount, with the API reflecting the change. - **Configured-but-unmounted remote shares are now reported** (issue #115; resolves tinkerologie/ha-unraid-management-agent#79) — the collector parses the plugin's `samba_mount.cfg` and merges it with `/proc/mounts`, so shares that are configured but not currently mounted appear with `status: "unmounted"` (plus their `auto_mount`/`read_only` metadata), enabling mount/unmount toggles in Home Assistant for offline shares. - **MCP `get_remote_shares` tool** (issue #115) — a dedicated read-only tool returning SMB/NFS/ISO remote share mount status and space usage. - **MQTT Home Assistant discovery for remote shares** (issue #115) — each remote share now publishes a `mounted` binary sensor plus usage / used / free capacity sensors, mirroring the existing unassigned-device entities. - **Agent Core (Phase 3 — Memory, Planner, Learning, Multi-turn Chat):** - **Episodic memory with semantic recall:** when a session finishes the agent writes an incident record (signature, goal, outcome, summary, action tags) to `agent_memory.json`. At the start of each new session the agent recalls the most relevant past incidents via keyword / tag matching (top-K, default 3) and injects them as context before the first LLM call. Active operator preferences are included in the same context injection. Controlled by new config fields `memory_enabled` (default `true`), `max_incidents` (default `200`), and `recall_top_k` (default `3`). - **Goal-decomposition planner:** operator-initiated sessions run one extra LLM call before the ReAct loop that decomposes the goal into a short ordered plan (`sess.Plan`, list of `{intent, tool}` steps) and injects a plan summary into the transcript. The call is best-effort — a planner failure never aborts the session. - **Suggest-not-mutate learning:** the agent can call two new read-only tools during a session: `propose_preference` (stores a `PENDING` autonomy / runbook preference) and `propose_runbook` (stores a proposed runbook in `agent_runbooks.json` alongside the static reviewed catalogue). Proposals never take effect until an operator confirms them. A confirmed preference of subject type `auto_approve_tool` (where `subject` is a tool name) causes the policy gate to auto-approve calls to that tool (the forbid-list still wins). - **Multi-turn chat (SendMessage):** `POST /api/v1/agent/sessions/{id}/messages` with body `{"message":"…"}` continues a completed or failed session with a new operator message and re-runs the ReAct loop. The conversation history is preserved across turns. - **New REST endpoints:** - `POST /api/v1/agent/sessions/{id}/messages` — send a follow-up message to a finished session. - `GET /api/v1/agent/memory` — returns `{incidents, preferences}` from the in-memory store. - `POST /api/v1/agent/preferences/{id}/confirm` — confirm (activate) a pending learned preference by ID. - **New MCP tools:** `agent_send_message`, `agent_get_memory`, `agent_confirm_preference` — exposing multi-turn chat, memory inspection, and preference confirmation to external MCP clients. - **New `agent_config.json` fields:** `memory_enabled` (bool, default `true`), `max_incidents` (int, default `200`), `recall_top_k` (int, default `3`). - **Agent Core (Phase 2 — Autonomy & Approval):** - **Event-driven triggers:** the alerting Engine and watchdog Runner publish a `dto.AgentWakeEvent` to the `agent_wake` typed pub-sub topic whenever an alert fires or a health check fails. The agent subscribes at daemon startup (before collectors start) and spawns autonomous investigation sessions automatically. Wake events are debounced (default 30 s), deduplicated by subsystem, and rate-limited by a per-subsystem cooldown (default 300 s). A concurrency cap (default 2) prevents more than a configurable number of parallel autonomous sessions. - **Approval gate (pause / resume):** when the agent proposes a tool whose risk tier maps to `approve`, the session pauses with status `awaiting_approval` and a `pending_approval` object (`action_id`, `tool`, `args`, `risk_tier`, `reason`). The full conversation transcript is persisted to disk so the session survives a daemon restart. Approving or denying the action resumes the loop from where it left off. An automatic TTL sweeper (default 3 600 s / 1 hour) denies any approval that has not been resolved within the configured window. - **Non-overridable forbid-list:** a hard-coded list of irreversible disk operations (`format_disk`, `clear_parity`, `disable_parity`, `partition_disk`, `delete_array_disk`) that the agent will describe but the gate **never** executes, even with explicit operator approval. The list is also configurable via the `forbid_list` field in `agent_config.json`. - **REST endpoints:** `POST /api/v1/agent/sessions/{id}/approve` (body `{"action_id":"…","approve":true|false}`) approves or denies a pending action and resumes the session; `POST /api/v1/agent/sessions/{id}/cancel` cancels a running or awaiting-approval session. - **MCP agent tools:** `agent_start_session`, `agent_get_session`, `agent_list_sessions`, `agent_approve_action` — exposing the full agent lifecycle to external MCP clients. - **New WebSocket events** on the `agent_stream` topic: `agent_approval_required` (session paused, pending action details in payload) and `agent_session_cancelled`. - **OpenAI-compatible LLM providers:** in addition to `"anthropic"`, the `provider` field now accepts `"openai"`, `"openrouter"`, and `"gemini"`. OpenRouter uses `https://openrouter.ai/api/v1/chat/completions` by default; Gemini uses `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`. All providers read the API key from `UMA_AGENT_API_KEY`. Example free model via OpenRouter: `openai/gpt-oss-20b:free`. - **New `agent_config.json` fields:** `wake_debounce_secs` (default `30`), `wake_cooldown_secs` (default `300`), `max_concurrent_sessions` (default `2`), `approval_ttl_secs` (default `3600`), `forbid_list` (`[]string`). - **Agent Core (Phase 1):** embedded autonomous operator with a pluggable LLM provider (Anthropic), a risk-tiered tool registry (read-only + low-risk auto-execute; high-risk reserved for approval), a bounded ReAct reasoning loop with iteration/token/deadline caps, JSON-persisted sessions, REST endpoints under `/api/v1/agent`, and a WebSocket `agent_stream` event feed. Disabled by default; opt-in via `agent_config.json` (`enabled: true`) and the `UMA_AGENT_API_KEY` environment variable. - **Continuous Docker container update detection** — a new background `docker_update` collector (default 6 h interval, staggered start, registry-rate-limit-safe) performs periodic digest comparisons for all running containers without blocking normal Docker polling. - `update_status`, `update_available`, and `update_checked` fields exposed on every container in `GET /api/v1/docker`, `GET /api/v1/docker/{id}`, MCP `list_containers`, and MCP `get_container_info`. - `GET /api/v1/docker/updates` now serves the cached result instantly (no live check on request; populated by the background collector). - `POST /api/v1/docker/updates/refresh` triggers an immediate re-check outside the normal schedule and publishes the result via the event hub. - MCP tool `refresh_container_updates` exposes the same on-demand refresh to AI agents. - `ContainerUpdatesAvailable` alerting metric — counts containers with an update ready; usable in alert rule expressions. - Opt-in startup notification when new container updates are detected: `--docker-update-notify` flag / `DOCKER_UPDATE_NOTIFY=true` env variable. - **Trend / predictive alerting** — in-memory ring-buffer `MetricsHistory` samples key metrics continuously. New alerting-expression fields: - `ArrayFillETAHours` — hours until the array is full at current fill rate - `MaxDiskFillETAHours` — hours until the fastest-filling individual disk is full - `CPUTempSlopePerMin` — CPU temperature trend (°C / min) - `MaxDiskTempSlopePerMin` — steepest disk temperature rise across all disks (°C / min) - `MaxContainerRestartsPerHour` — highest container restart rate over the sampled window - `MaxReallocatedSectors` — maximum reallocated sector count across all array disks - `MaxPendingSectors` — maximum pending sector count across all array disks - `DiskErrorsIncreasing` — `true` when any disk's error count is trending upward - Disk SMART attributes (`Reallocated_Sector_Ct`, `Current_Pending_Sector`) now collected via `smartctl -A` and surfaced in the alerting environment. - **Alert rule templates** — `GET /api/v1/alerts/templates` and MCP tool `list_alert_templates` return five curated, disabled-by-default rule templates using trend/predictive metrics. Users can review, copy, and enable them in their rule config. - **One-click alert template enable** — `POST /api/v1/alerts/templates/{id}/enable` and MCP tool `enable_alert_template` instantiate and enable an alert rule directly from a template ID (e.g. `tmpl-array-fill`) in a single call. Idempotent — re-posting updates the existing rule without duplication. Defaults notification channels to `["unraid"]` (Unraid built-in notifications) when no `channels` body is provided. - **Container network I/O** — `network_rx_bytes`, `network_tx_bytes`, `network_rx_bytes_per_sec`, and `network_tx_bytes_per_sec` fields now populated on every container in `GET /api/v1/docker` and `GET /api/v1/docker/{id}` via sampling from `/proc//net/dev`. `restart_count` field also added to each container. - **Docker networks** — new `GET /api/v1/docker/networks` endpoint and MCP tool `list_docker_networks` expose all Docker networks with driver, scope, IPAM subnet / gateway, and the list of connected container names. - **Continuous plugin update detection** — a new background `plugin_update` collector (configurable via `INTERVAL_PLUGIN_UPDATE`, default 6 h) checks all installed plugins for updates and caches the result. `POST /api/v1/plugins/updates/refresh` and MCP tool `refresh_plugin_updates` trigger an immediate re-check and publish the result via the event hub. `PluginUpdatesAvailable` alert metric counts plugins with a pending update. - **OS update availability (local-only)** — new `GET /api/v1/os/update` endpoint and MCP tool `get_os_update` return the cached OS update status sourced entirely from local files (`/etc/unraid-version`, `/tmp/plugins/update/`). No outbound network calls are made. A background `os_update` collector refreshes this on a configurable interval (`INTERVAL_OS_UPDATE`, default 24 h). Status values: `up_to_date`, `update_available`, `unknown`. - **Mover status** — new `GET /api/v1/mover` endpoint and MCP tool `get_mover_status` expose the cached mover state: whether it is currently active, the cron schedule from `var.ini`, and last-run statistics (start/finish timestamps, duration, files moved, bytes moved) parsed from `/var/log/mover.log`. Refreshed by the new `mover` collector (`INTERVAL_MOVER`, default 5 min). - **AI remediation toolkit:** - **System health report** — `GET /api/v1/health/report` and MCP tool `system_health_report` aggregate health signals from the array, disks, containers, and firing alerts into a prioritised findings list with recommended actions. The MCP tool is read-only by default; passing `confirm: true` together with an `actions` list (from a previous report) executes supported actions (`start/stop/restart_container`, `start/stop/restart/force_stop_vm`) via the remediation executor. - **Metric history query** — `GET /api/v1/metrics/history?metric=&entity=` and MCP tool `query_metric_history` return all buffered ring-buffer samples plus summary statistics (slope per second, min, max, average, last value) for any tracked metric. Global metrics: `cpu_temp`, `array_used_pct`. Per-entity metrics: `disk_temp`, `disk_used_pct`, `disk_errors`, `reallocated`, `pending`, `restart_count`. - **Runbook catalogue** — MCP tool `list_runbooks` lists reviewed remediation runbooks. `run_runbook` dry-runs or executes a named runbook (requires `confirm: true` to execute). `find_root_cause` correlates cached system signals (CPU, array, parity, disk temperatures, containers) to surface the most likely root causes of degraded performance or health. - **New collectors:** `docker_networks`, `plugin_update`, `os_update`, `mover` — each follows the standard collector pattern with panic recovery, configurable polling interval, and WebSocket / event-hub publishing on each update. - **New environment variables / CLI flags for collector intervals:** `INTERVAL_DOCKER_NETWORKS`, `INTERVAL_PLUGIN_UPDATE`, `INTERVAL_OS_UPDATE`, `INTERVAL_MOVER`. ### Changed - **More robust `parity_valid` determination** (issues #98, #114) — array parity validity now corroborates the `var.ini` `sbSynced` signal against the parity-checks log: when the array is started with parity disks and the most recent parity check/sync completed successfully (exit 0, zero errors), parity is reported valid even on Unraid versions that omit or zero out `sbSynced`. This corroboration is additive (it can only confirm validity, never mask sync errors). Added comprehensive debug logging of every signal feeding the decision (`sbSynced`, `sbSynced2`, `sbSyncErrs`, `sbSyncExit`, `mdNumInvalid`, `mdResync`, `numParityDisks`, state) to aid future diagnosis. - **`GET /api/v1/docker/updates`** now returns the cached result immediately instead of performing a live registry check on each request; use the new `POST /api/v1/docker/updates/refresh` to force an on-demand re-check. - **Runtime collector interval cap** raised from 3 600 s to 86 400 s (24 h) to accommodate the new `docker_update` collector's default 6 h schedule. ### Fixed - **Remote shares (SMB/NFS) never reported** (issue #115; resolves tinkerologie/ha-unraid-management-agent#83 and tinkerologie/ha-unraid-management-agent#79) — the unassigned-devices collector's `parseSMBMounts()` was a stub that always returned an empty list, and NFS mounts were not parsed at all, so `remote_shares` was permanently empty across the REST API (`/api/v1/unassigned`, `/api/v1/unassigned/remote-shares`), MQTT, and MCP. As a result Home Assistant could never create binary sensors for remote shares. The collector now parses `/proc/mounts` for `cifs`/`smb3`/`smbfs` and `nfs`/`nfs4` filesystems mounted under `/mnt/remotes/` and `/mnt/disks/`, populating server/share/export fields, read-only status, and capacity (size/used/free/usage via `statfs`). Octal-escaped mount fields (e.g. spaces) are decoded. ISO loop mounts are still detected. - **MCP `get_unassigned_devices` hid remote shares** (issue #115) — the tool returned "No unassigned devices found" whenever the local device list was empty, even when remote shares were present. It now returns data when either `Devices` or `RemoteShares` is non-empty. - **MCP `get_parity_history` always returned "No parity check history available"** (issue #114) — the cache layer's `GetParityHistoryCache()` was a stub that unconditionally returned an empty result, so the MCP tool (and the parity section of `system_health_report` / `get_diagnostic_summary`) never reported any history even when `/boot/config/parity-checks.log` was populated. It now loads and caches the real parity-checks log (60 s TTL), matching the `GET /api/v1/array/parity-check/history` REST endpoint. Verified live: REST, MCP, and the on-disk log all report identical record counts. ### Security - **Upgraded Go toolchain 1.26.3 → 1.26.4** — picks up the standard-library security fixes in Go 1.26.4 (released 2026-06-02): CVE-2026-27145 (`net/textproto`, reached via `net/http` response-header parsing — relevant to the daemon's outbound HTTP), CVE-2026-39822 (`crypto/x509`), and CVE-2026-42504 (`mime`). Bumped the `go` directive in `go.mod` and the `setup-go` version in the release workflow. ## [2026.06.00] - 2026-05-30 ### Added - **Unraid 7.3 support — new data exposed across the API/MQTT/MCP:** - **Chassis serial number** in `/api/v1/hardware` (`chassis` block: manufacturer, type, version, serial, asset tag) via sysfs DMI with dmidecode (type 3) fallback. New HA sensor `Hardware: Chassis Serial`. - **TPM presence/version** in `/api/v1/hardware` (`tpm` block) from `/sys/class/tpm/tpm0` — supports Unraid 7.3 TPM-based licensing. New HA binary sensor `Hardware: TPM Present`. - **Boot device / boot pool** in `/api/v1/hardware` (`boot` block: device type usb vs internal, backing device, filesystem, ZFS boot pool) — Unraid 7.3 internal-boot support. New HA sensor `Hardware: Boot Device Type`. - **ZFS corrupted files** per pool (`corrupted_files`) parsed from `zpool status -v` (ZFS 2.4.1 surfaces these without a scrub), plus `is_boot_pool` flag. New HA sensor `ZFS: Corrupted Files`. - **ZFS configured ARC max** (`configured_max_bytes`) from `/sys/module/zfs/parameters/zfs_arc_max` (0 = auto) — Unraid 7.3 first-class tunable. New HA sensor `ZFS ARC: Configured Max`. - **Docker container MAC address** (`mac_address`) from `docker inspect` (Docker 29 / Unraid 7.3 fixed-MAC support). New per-container HA sensor. - **Per-process disk I/O** via new `GET /api/v1/processes/io` and MCP tool `list_process_io` — top processes by I/O rate sampled natively from `/proc//io` (lighter than spawning iotop-c, no always-on cost). - **New alert-rule fields:** `ZFSCorruptedFiles`, `BootPoolHealthy`, `BootPoolHealth` (degraded ZFS boot pool / corrupted files alerting). - **Swap memory metrics in `/api/v1/system`** — the system collector now reports `swap_total_bytes`, `swap_used_bytes`, `swap_free_bytes`, and `swap_usage_percent` parsed from `/proc/meminfo`, plus the kernel `swappiness` tunable from `/proc/sys/vm/swappiness` (`-1` when unavailable). Addresses the Home Assistant integration swap-sensor request (ha-unraid-management-agent #45). - **Swap & swappiness Home Assistant sensors** — new MQTT discovery sensors (`swap_usage`, `swap_used`, `swap_free`, `swap_total`, `swappiness`). - **Swap fields available to the alerting engine** — `SwapUsedPct`, `SwapTotalBytes`, `SwapUsedBytes`, and `SwapFreeBytes` can now be used in alert rule expressions. - **Home Assistant notification event entity** — a new MQTT `event` entity (`notifications/event` topic) fires a Home Assistant event for each new Unraid notification, exposing the full notification details (id, title, subject, description, importance, type, link, timestamp) as event attributes. The existing backlog is seeded silently on startup so a restart does not replay old notifications. ### Security - **Fixed reachable vulnerability GO-2026-5013** — bumped `golang.org/x/crypto` v0.51.0 → v0.52.0 (byte-arithmetic underflow/panic in `golang.org/x/crypto/ssh`, reachable via libvirt `ConnectToURI` → `ssh.Dial` in the VM collector). `govulncheck ./...` now reports zero vulnerabilities. ### Changed - **Dependencies bumped** — `github.com/nicholas-fedor/shoutrrr` → v0.15.1, `golang.org/x/net` → v0.55.0, `golang.org/x/sys` → v0.45.0, `golang.org/x/crypto` → v0.52.0; `go mod tidy` run. `go vet` clean. - **Unraid 7.3 regression checks (no change required):** verified RAM reporting is unaffected by the 7.3 dmidecode unit-label change (memory device size is a passthrough string; system memory comes from `/proc/meminfo`), and confirmed disk SMART polling already uses `smartctl -n standby` so routine collection never wakes spun-down drives (aligns with 7.3's "don't spin up the array on reads" fix). ## [2026.05.00] - 2026-05-16 ### Changed - **Go version upgraded to 1.26.3** — Latest stable Go release with improved performance (Green Tea GC, ~30% faster cgo, ~2x faster `io.ReadAll`) - **All dependencies updated to latest versions** — 16 direct dependencies and 30+ transitive dependencies bumped to their latest compatible versions including: - `go.opentelemetry.io/*` → v1.43.0 - `github.com/prometheus/common` → v0.67.5 - `github.com/prometheus/procfs` → v0.20.1 - `golang.org/x/*` tooling packages (crypto, mod, net, sys, term, tools) - `github.com/go-openapi/swag/*` → v0.26.0 - All other dependencies audit-tested with `govulncheck` — zero vulnerabilities found ### Fixed - **ZFS cache/pool disk usage in `/api/v1/disks`** — disk collection now resolves ZFS-backed cache and pool members to their owning zpool and uses `zpool list` capacity data instead of `statfs()` on `/mnt/`, fixing near-zero usage on root datasets and `null` usage for mirrored members like `cache2` Thanks for the contribution @laurensguijt ## [2026.04.01] - 2026-04-10 ### Fixed - **Individual GPU metrics for multi-vendor systems** — GPU indices are now globally unique across all vendors (Intel → NVIDIA → AMD ordering), fixing MQTT topic collisions and Home Assistant entity conflicts when multiple GPU vendors are present (e.g., Intel iGPU + NVIDIA discrete GPUs). Previously, each vendor assigned indices starting from 0, causing `unraid/gpu/0` to be shared by different GPUs. Closes #105 - **Stabilise ROCm GPU ordering** — AMD GPU collection via `rocm-smi` now sorts card identifiers numerically before processing, preventing non-deterministic index assignment across collection cycles - **Replace direct `exec.Command` usage** — replaced 2 occurrences in `unassigned.go` and 1 in `probes.go` with `lib.ExecCommandOutput` / `lib.ExecCommandOutputWithContext` wrappers for consistent command execution and error handling - **Race condition in DockerCollector** — added `sync.Mutex` to protect concurrent access to the `prevCPU` map in `getCPUFromCgroups` and `pruneStaleSnapshots` - **CORS missing PATCH method** — added `PATCH` to `Access-Control-Allow-Methods` in CORS middleware so preflight requests for `PATCH /api/v1/collectors/{name}/interval` succeed - **WebSocket localhost alias matching** — added `isLocalhost()` equivalence check in WebSocket `CheckOrigin` for consistency with CSRF middleware (allows `localhost`/`127.0.0.1`/`::1` to match interchangeably) - **Zombie processes from background scripts** — replaced `proc.Release()` with `go proc.Wait()` in `executeScriptBackground` to properly reap child processes - **lsblk stderr contaminating JSON** — added `ExecCommandStdout` helper and switched `getDeviceInfo` in unassigned collector to use stdout-only output, preventing stderr warnings from corrupting JSON parsing - **Ping target validation** — added `ValidateHostOrIP` validator and applied it in `probePing` to reject empty, flag-prefixed, or malformed targets before execution ### Security - **Sanitize verbose error responses** — Removed raw error details from 40+ REST API 500 Internal Server Error responses. Internal errors are now logged server-side only; clients receive generic error messages without implementation details (OWASP A05:2021) - **Upgrade OpenTelemetry to v1.41.0** — Fixes CVE-2026-29181 (CWE-770 uncontrolled resource consumption) in go.opentelemetry.io/otel v1.39.0 - **Add global rate limiting middleware** — New rate limiter (10 req/s, burst of 20) mitigates denial-of-service and brute-force attacks on REST API endpoints - **Restrict CORS default** — CORS headers are no longer set when `CORS_ORIGIN` is not explicitly configured. Previously defaulted to `Access-Control-Allow-Origin: *` which allowed any website to make cross-origin API requests. Set `CORS_ORIGIN` explicitly to restore cross-origin access if needed - **Warn on insecure MQTT TLS** — Log a warning at startup when MQTT `InsecureSkipVerify` is enabled to flag insecure TLS configurations - **Fix shell injection in userscripts controller** — removed the `sh -c` + `fmt.Sprintf` command-construction pattern in favor of direct argument passing to eliminate CWE-78 command injection risk - **WebSocket origin validation** — added per-request origin checking that validates the Origin header host against the request Host; added 64 KB `ReadLimit` to prevent message-size DoS - **Security headers middleware** — added `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection`, `Content-Security-Policy`, `Referrer-Policy`, and `Permissions-Policy` headers to all HTTP responses - **CSRF origin validation middleware** — validates Origin header on state-changing requests (POST/PUT/PATCH/DELETE) with localhost-aware matching - **Request body size limit** — added 1 MB `MaxBytesReader` middleware to prevent request body DoS across all endpoints - **HTTP server timeout hardening** — added `ReadHeaderTimeout` (10 s) and `IdleTimeout` (120 s) to mitigate slowloris-style attacks - **Null byte injection protection** — added CWE-158 null byte checks to `ValidateShareName`, `ValidateUserScriptName`, `ValidatePluginName`, and `ValidateSnapshotName` - **Consistent validation error reporting** — refactored `ValidateLogFilename` from `bool` return to `error` return with descriptive messages matching other validators ## [2026.04.00] - 2026-04-08 ### Security - **Fix shell injection vulnerability in start script** — replaced unsafe `bash -c` with unquoted variable interpolation with `env(1)` for safe variable passing; added input sanitization helpers (`sanitize_int`, `sanitize_csv`, `sanitize_str`, `sanitize_bool`) to validate all config values - **Sanitize all config values** — integer-only validation for ports/intervals, boolean allowlist for flags, shell metacharacter stripping for freeform strings, LOG_LEVEL allowlist ### Added - **Log action handler in exec.php** — new `log` action returns last 20 log lines for display in settings UI - **Default config additions** — added `LOG_LEVEL` and `MQTT_CLIENT_ID` defaults to `default.cfg` - **Diagnostic Logging System** ([#102](https://github.com/tinkerologie/unraid-management-agent/issues/102)): - **Structured diagnostic logger** — JSON Lines format with lumberjack rotation (5 MB max, 1 backup) - **Correlation IDs** — UUID-based request/operation correlation via context propagation - **Sensitive data redaction** — regex-based redaction for passwords, bearer tokens, Shoutrrr URLs, webhooks, CSRF tokens; reflection-based struct/map redaction for sensitive field names - **Diagnostic bundle CLI** — `diagnostics` command collects system state, array status, containers, VMs, network info, logs, and configuration into a timestamped ZIP archive - **Diagnostic bundle service** — collects metadata, system state from `/proc`, array/disk info, Docker/VM listings, network interfaces, agent logs, and redacted configuration - New DTOs: `DiagnosticLogEntry`, `DiagnosticBundle`, `BundleMetadata`, `BundleSystemState`, `BundleArrayStatus`, `BundleDisk`, `BundleContainer`, `BundleVM`, `BundleNetwork`, `BundleLogs`, `BundleConfiguration` ### Fixed - **Concurrent map access in DiskCollector** ([#100](https://github.com/tinkerologie/unraid-management-agent/pull/100)): - Added `sync.Mutex` to protect shared map access during disk data collection ## [2026.03.06] - 2026-03-30 ### Added - **System Tuning (Tips & Tweaks)** — new collector, controller, REST endpoints, and MCP tools: - **Turbo Boost**: read/write Intel Turbo Boost and AMD Performance Boost via sysfs - **Disk Cache**: read/write `vm.dirty_*` kernel parameters (background ratio, ratio, writeback, expire) - **Inotify Limits**: read/write `fs.inotify.*` kernel parameters (max watches, instances, queued events) - **NIC Offloads**: read hardware offload feature states via `ethtool -k` (rx/tx checksumming, scatter-gather, TSO, GSO, GRO, LRO, VLAN) - **NIC Ring Buffers**: read ring buffer sizes via `ethtool -g` (RX/TX max and current) - REST API: `GET /api/v1/tuning`, `POST /api/v1/tuning/turbo`, `POST /api/v1/tuning/disk-cache`, `POST /api/v1/tuning/inotify` - MCP tools: `get_tuning_status`, `set_turbo_boost`, `set_disk_cache`, `set_inotify_limits` - Tuning controller restores original values on agent shutdown - Configurable collection interval (default 120s, env `INTERVAL_TUNING`) ### Fixed - **Agent keeps entering stopped state due to unrecovered panics** ([#99](https://github.com/tinkerologie/unraid-management-agent/issues/99)): - Added `LogPanicWithStack` helper for stack-trace-enriched panic logging across all recovery blocks - Added missing panic recovery to Docker, GPU, and VM collectors (previously had none) - Fixed Notification collector: restructured from top-level-only recovery to per-iteration recovery - Added top-level crash recovery to Alerting engine and Watchdog runner - Wrapped all Orchestrator goroutines (alert engine, watchdog, HTTP server, MQTT, STDIO mode) with panic recovery - Upgraded all existing collector recovery blocks to use `LogPanicWithStack` for better crash diagnostics - Added lifecycle logging: "Agent startup complete" and "Shutdown complete" messages ## [2026.03.05] - 2026-03-30 ### Fixed - **`parity_valid` returns false on Unraid 7.2.4 when parity is actually valid** ([#98](https://github.com/tinkerologie/unraid-management-agent/issues/98)): - Added `mdNumInvalid` fallback for parity validity when `sbSynced` is missing or zero - A started array with parity disks and zero invalid disks is now correctly reported as parity-valid - Fixes false "Parity Invalid" repair notifications in the Home Assistant integration - Added debug logging of `sbSynced`, `mdNumInvalid`, and `sbSyncErrs` values during array collection - **CPU temperature sensor returns incorrect/stuck value (127.5°C) on some hardware** ([#97](https://github.com/tinkerologie/unraid-management-agent/issues/97)): - CPU and motherboard temperature extraction now uses `IsPlausibleTempC()` to reject readings outside the -40°C to 125°C range - Filters out stuck 127.5°C TjMax sentinel values reported by some hwmon sensors on Intel systems - Motherboard temp check unified to use the same plausibility function instead of a hardcoded 0–100°C range ## [2026.03.04] - 2026-03-29 ### Added - **Full fan control system** — monitor and control chassis fans via REST API, WebSocket, MCP, and Prometheus: - New `GET /fans` endpoint returning per-fan RPM, PWM %, mode, and controllability - `POST /fans/speed` — set fan speed by percentage (manual mode) - `POST /fans/mode` — switch between off/manual/automatic modes - `POST /fans/profile` — assign a temperature-responsive fan curve profile to a fan - `POST /fans/profile/create` — create custom fan curve profiles with temperature→speed points - `POST /fans/defaults` — restore all fans to their original state - `PUT /fans/config` — enable/disable fan control and update safety thresholds - Hwmon-based fan discovery scanning `/sys/class/hwmon` for all fan channels and PWM controls - Fan curve engine with linear interpolation between temperature→speed curve points - Built-in profiles: silent, balanced, performance, full-speed - Safety guard with emergency full-speed on critical temperature, fan stall detection, minimum speed enforcement, and original state capture/restore on shutdown - IPMI fan provider stub for future BMC-based fan control - Fan control collector publishing `fan_control_update` events to the event bus - WebSocket broadcast of real-time fan status changes - 6 MCP tools: `get_fan_status`, `set_fan_speed`, `set_fan_mode`, `create_fan_profile`, `assign_fan_profile`, `restore_fan_defaults` - 10 Prometheus gauges: `unraid_fan_rpm`, `unraid_fan_pwm_percent` per fan - MQTT Home Assistant discovery for per-fan sensor entities - Input validation for fan IDs, speed percentages, mode values, and profile names - **MQTT Home Assistant sensor coverage for network throughput rates** ([#96](https://github.com/tinkerologie/unraid-management-agent/pull/96)): - New `RxBytesPerSec` and `TxBytesPerSec` fields in `NetworkInfo` DTO for per-interface throughput - Delta-based rate calculation with mutex-protected previous sample state - Stale interface entries pruned automatically when interfaces disappear - **MQTT Home Assistant discovery for fan sensors** ([#96](https://github.com/tinkerologie/unraid-management-agent/pull/96)): - Dynamic per-fan HA sensor entities discovered from system fan data - Fan discovery goroutine publishes entities on first `PublishSystemInfo` call - **MQTT Home Assistant discovery for NUT UPS, hardware, registration, unassigned devices, and ZFS** ([#96](https://github.com/tinkerologie/unraid-management-agent/pull/96)): - 7 new MQTT Publish methods: NUT, Hardware, Registration, Unassigned, ZFS Datasets, ZFS Snapshots, ZFS ARC Stats - 7 new `mqttBind` entries in orchestrator for new event topics - HA discovery entities for NUT UPS sensors, hardware info, registration/license, per-unassigned-device sensors, per-ZFS-dataset sensors, ZFS snapshot aggregates, and ZFS ARC stats - **Context-aware shell command execution** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - New `ExecCommandOutputWithContext()` in `daemon/lib/shell.go` for commands that need timeout/cancellation support - **ZFS per-share sizing** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - Share collector now queries ZFS dataset sizes via `zfs list` for `useCache=only` shares - 60-second timeout prevents a hung `zfs list` from blocking the share collector goroutine ### Fixed - **Share sizes reported at ~50% of actual size** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - Unraid's share `.ini` files store sizes in KiB, but the share collector multiplied by 1 (treating as bytes) - Changed to multiply by 1024 so `free_bytes` and `used_bytes` report correct values - **Notification collector reads paths from dynamix.cfg** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - `ResolveNotificationDirs()` now reads the Dynamix notification directory from `dynamix.cfg` at startup - Notification controller resolves paths via `collectors.ResolveNotificationDirs` at `init()` so create/archive/delete target the correct directories - Dynamix `"normal"` importance now correctly maps to `"info"` in notification counts - **Dark mode UI fixes for plugin settings page** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - Plugin page now uses proper Unraid CSS variables for dark theme compatibility - **Log spammed with UPS and fan warnings** ([#95](https://github.com/tinkerologie/unraid-management-agent/issues/95)): - Fan speed "no fan sensors found" downgraded from `Warning` to `Debug` — no longer spams logs every 15 seconds on systems without fan sensors - NUT UPS fallback failure downgraded from `Warning` to `Debug` - Fixed broken printf format strings in 19 logger calls across system, ZFS, and UPS collectors that used structured-logging style args (`"msg", "key", value`) instead of printf verbs (`"msg: %v", value`), causing `%!(EXTRA ...)` garbage in log output - **UPS collector panic recovery** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - Periodic `Collect()` call now wrapped in defer/recover to prevent a panic from killing the goroutine - **MQTT nil dereference guard** ([#96](https://github.com/tinkerologie/unraid-management-agent/pull/96)): - `PublishSystemInfo` now guards against nil `SystemInfo` to prevent panic on first publish before data is available - **Fan speed parsing improved for `sensors -u` format** ([#96](https://github.com/tinkerologie/unraid-management-agent/pull/96)): - Fan RPM parsing now uses `ParseFloat` for the raw `sensors -u` output format - Hwmon fan labels simplified to `"Fan %d"` format - Zero RPM fan channels (no fan connected) are skipped - **Bogus sensor filtering hardened against unreliable motherboard hardware**: - Fan RPM readings above 25 000 RPM (`MaxPlausibleRPM`) are treated as non-detected — fixes fan5 reporting 56 784 RPM from a bogus hwmon sensor - Temperature sensors with known-unreliable labels (AUXTIN, SYSTIN, intrusion) are now skipped in `ReadMaxHwmonTemp()` — prevents phantom readings from unpopulated Nuvoton/ITE chipset headers triggering false emergency full-speed - Plausible temperature range tightened from 150 °C to 125 °C to reject I²C/SMBus "not connected" sentinel values (127–128 °C) - Fan curve per-sensor temperature reads now validated with `IsPlausibleTempC()` before use in speed interpolation ### Changed - **APC UPS fallback log level** ([#93](https://github.com/tinkerologie/unraid-management-agent/pull/93)): - `apcaccess` failure message downgraded from `Warning` to `Debug` to reduce log noise on systems without APC UPS ## [2026.03.03] - 2026-03-18 ### Fixed - **FileWatcher race condition causing silent agent exits every 1–4 hours** ([#84](https://github.com/tinkerologie/unraid-management-agent/issues/84)): - Moved `fw.Close()` inside the goroutine running `fw.Run()` in array, disk, and share collectors so the fsnotify channels are no longer closed while the watcher loop is still selecting on them - Added warning-level logging in `filewatcher.go` when the events or errors channel closes unexpectedly, making any future silent exits visible in the log - **Removed dead `ENABLE_UPS` and `ENABLE_GPU` config flags from PLG template** ([#83](https://github.com/tinkerologie/unraid-management-agent/issues/83)): - The flags had no effect — collectors always started regardless of their value - Setting `INTERVAL_UPS=0` or `INTERVAL_GPU=0` (or any collector interval to `0`) is the supported way to disable collectors, now documented in the config template comment ## [2026.03.02] - 2026-03-16 ### Fixed - **WebSocket client eviction no longer mutates the hub under a read lock**: - Broadcast now snapshots clients before sending and removes stale clients under an exclusive lock, avoiding concurrent map mutation under load - **Unassigned device filesystem sizing no longer shells out to `df`**: - Mounted partition and remote share usage now uses native `statfs` calls, reducing process overhead and removing subprocess findings - **Collector runtime shutdown state is now cleared explicitly**: - Collector cancellation is released on disable, restart, stop-all, and collector exit, with regression coverage for runtime state cleanup - **MQTT QoS is normalized before publish/subscribe operations**: - Invalid QoS values now fall back safely to `0` instead of flowing through unchecked conversions - **Alert message formatting avoids unnecessary temporary strings**: - Dispatcher now writes formatted content directly to the builder - **Parity check history endpoint no longer returns invalid JSON on systems without parity** ([#82](https://github.com/tinkerologie/unraid-management-agent/issues/82)): - `parseSpeed()` now guards against `NaN` and `Inf` float64 values that `strconv.ParseFloat` accepts but `encoding/json` cannot serialize - Systems that have never run a parity check (e.g., full ZFS setups) no longer trigger `json: unsupported value: NaN` errors every 30 seconds ### Changed - **Plugin PLG now includes `project` and `readme` attributes** per [plugin-docs](https://github.com/mstrhakr/plugin-docs) best practices - **CA XML template uses `` root element** instead of legacy `` wrapper per plugin-docs recommendations - **Removed the duplicate template PLG from the release process**: - `unraid-management-agent.plg` is now the only maintained PLG file - Release automation and deploy tooling no longer reference `meta/template/unraid-management-agent.plg` - **Retired redundant root maintenance scripts in favor of Ansible**: - Local deploy, live validation, and diagnostics now flow through `ansible/deploy.yml` - Ansible deploy remains cleanup-focused and does not create pre-deploy backup directories on the Unraid host - **Fixed the Ansible deployment summary counts**: - Prometheus metric families and collector totals now use the same underlying verify data shown by the dedicated verification tasks ### Security - **Log file reads are restricted to the known allowlist**: - Arbitrary filesystem paths are rejected before stat/open, preserving the log API while removing path-traversal findings - **Security annotations were narrowed and documented for trusted system paths and direct argv process execution**: - Raw `gosec`, `golangci-lint`, and the repo security checks now run cleanly with explicit rationale at each trusted boundary - **Upgraded `modelcontextprotocol/go-sdk` from v1.2.0 to v1.3.1** to fix GO-2026-4569 (improper handling of case sensitivity) - **Bumped Go from 1.26.0 to 1.26.1** (latest patch release) ## [2026.03.01] - 2026-03-05 ### Fixed - **`io_utilization_percent` reports cumulative I/O milliseconds, not a utilization percentage** (GitHub Issue #81): - `enrichWithIOStats` previously divided the cumulative `io_ticks` counter from `/sys/block/*/stat` by 10, producing unbounded values (e.g. 29,832,950%) that grew indefinitely with uptime - Now uses a delta-based calculation: tracks previous `io_ticks` per device and computes `(delta_io_ticks / delta_wall_time) * 100`, yielding a proper 0–100% utilization value - On the first collection after startup, the field is omitted (no previous sample to compare against) ## [2026.03.00] - 2026-03-01 ### Fixed - **EventBus goroutine leak** (GitHub Issue #67): - `Unsub()` now closes the channel when it is no longer subscribed to any topic, unblocking `for msg := range ch` goroutines - **CollectorManager broadcastStateChange deadlock** (GitHub Issue #68): - `EnableCollector()` and `DisableCollector()` now snapshot event data under lock, then publish outside the lock to prevent deadlock with subscribers calling `GetStatus`/`GetAllStatus` - **GPUCount inflated by nil entries** (GitHub Issue #70): - `GPUCount` is now incremented inside the nil-guard loop, counting only non-nil GPU entries - **MQTT goroutine leak on reconnect** (GitHub Issue #71): - `handleConnect()` goroutines are now cancelled via context on disconnect/reconnect; discovery, state publishing, and command subscription run sequentially - **EventBus zero-capacity channel** (GitHub Issue #72): - `NewEventBus()` clamps `bufferSize < 1` to `1`, preventing zero-capacity channels that silently drop all messages - **WebSocket subscribe null resets topic filter** (GitHub Issue #73): - `readPump` now uses `json.RawMessage` envelope to distinguish absent `subscribe` key from explicit `null`, correctly resetting topic filter to "all topics" - **GetParityHistoryCache always returns nil** (GitHub Issue #76): - Returns `&dto.ParityCheckHistory{}` empty sentinel instead of `nil` so callers never need nil checks - **Test assertion improvements** (GitHub Issue #78): - Added nil guard to `subscribeMQTTEvents` for nil mqttClient - `TestSubscribeMQTTEvents_NilClient` asserts immediate return instead of relying on timeout - `TestSubscribeMQTTEvents_NotConnected` passes domainCtx instead of nil - EventBus tests updated for channel closure behavior; added `TestEventBus_UnsubPartial` - **VM cannot be resumed/started via API when pmsuspended** (GitHub Issue #65): - Fixed `POST /api/v1/vm/{name}/resume` and `POST /api/v1/vm/{name}/start` failing when VM is in pmsuspended state (Windows sleep) - libvirt DomainResume fails with "domain is pmsuspended"; DomainCreate fails with "domain is already running" - VM controller now detects pmsuspended state and uses `virsh dompmwakeup` to wake the VM, equivalent to pressing Start in Unraid web UI - **Disk size reported at ~50% of actual size** (GitHub Issue #80): - Unraid's `disks.ini` stores `size` in KiB (1024-byte blocks), but the disk collector multiplied by 512 (assuming sectors) - Changed multiplier from 512 to 1024 so `size_bytes` now reports the correct disk capacity - I/O statistics conversions (read/write bytes from `/sys/block/*/stat`) are unchanged as they correctly use 512-byte sectors per Linux kernel convention ### Changed - **Hardcoded topic strings replaced with constants** (GitHub Issue #74): - 11 hardcoded topic name strings in collector debug logs replaced with `constants.Topic*.Name` references - **Network services cache log level** (GitHub Issue #77): - `CacheStore.GetNetworkServicesCache()` failure log changed from `Debug` to `Warning` - **Swagger healthcheck docs updated** (GitHub Issue #79): - `HealthCheckConfig.Type` and `Target` field descriptions now include the `"ping"` probe type - **Nil-deref safety documented** (GitHub Issue #69): - Added comment in alerting engine documenting that `NotificationOverview` and `NotificationCounts` are value types (nil deref not possible) - **Go 1.26.0 Upgrade**: - Upgraded from Go 1.25.0 to Go 1.26.0 for improved performance and latest language features - **Green Tea GC**: 10–40% lower garbage collection overhead (enabled automatically by runtime) - **~30% faster cgo**: Faster native Docker/libvirt operations - **`io.ReadAll` ~2x faster**: Improved `/proc` and `/sys` file reading in all collectors - **Stack-allocated slices**: Fewer heap allocations in hot paths - Modernized 39 Go files via `go fix`: - `interface{}` → `any` across 20 files (98 occurrences) - C-style `for` loops → `for i := range n` across 21 files - `strings.Split` in range → `strings.SplitSeq` (zero-allocation iteration) - `sort.Slice` → `slices.SortFunc` (type-safe sorting) - `t.Context()` in test files (cleaner test contexts) - Refactored `sync.WaitGroup` patterns to use `wg.Go()` (Go 1.26) — eliminates `wg.Add(1); go func() { defer wg.Done()... }` boilerplate in orchestrator and controllers - Refactored `RunMCPStdio()` signal handling to use `signal.NotifyContext` — replaces manual goroutine + channel pattern - Updated CI workflow, devcontainer, and documentation references to Go 1.26 - All dependencies tested and compatible with Go 1.26 ## [2026.02.02] - 2026-02-21 ### Added - **Docker Container Update Management**: - Check individual container for image updates (`GET /docker/{id}/check-update`) - Check all containers for available updates (`GET /docker/updates`) - Get container disk size info (`GET /docker/{id}/size`) - Update individual container to latest image (`POST /docker/{id}/update`, supports `?force=true`) - Bulk update all containers with available updates (`POST /docker/update-all`) - MCP tools: `check_container_updates`, `check_container_update`, `get_container_size`, `update_container`, `update_all_containers` - **Plugin Update Management**: - Check all installed plugins for available updates (`GET /plugins/check-updates`) - Update individual plugin (`POST /plugins/{name}/update`) - Bulk update all plugins (`POST /plugins/update-all`) - MCP tools: `check_plugin_updates`, `update_plugin`, `update_all_plugins` - **VM Snapshots & Cloning**: - Create VM snapshot (`POST /vm/{name}/snapshot`) - List VM snapshots (`GET /vm/{name}/snapshots`) - Delete VM snapshot (`DELETE /vm/{name}/snapshots/{snapshot_name}`) - Restore VM snapshot (`POST /vm/{name}/snapshots/{snapshot_name}/restore`) (GitHub Issue #63) - Clone virtual machine (`POST /vm/{name}/clone?clone_name=...`) - MCP tools: `list_vm_snapshots`, `create_vm_snapshot`, `delete_vm_snapshot`, `restore_vm_snapshot`, `clone_vm` - **Docker Container Logs** (GitHub Issue #64): - Retrieve per-container stdout/stderr logs equivalent to `docker logs` (`GET /docker/{id}/logs`) - Supports `tail`, `since` (RFC3339), and `timestamps` query parameters - MCP tool: `get_container_logs` - **Service Management**: - List all managed services and their status (`GET /services`) - Start/stop/restart system services (`POST /services/{name}/{action}`) - Supported services: docker, libvirt, smb, nfs, ftp, sshd, nginx, syslog, ntpd, avahi, wireguard - MCP tools: `list_services`, `get_service_status`, `service_action` - **Process Listing**: - List running processes sorted by CPU/memory/PID (`GET /processes?sort_by=cpu&limit=50`) - MCP tool: `list_processes` - **New Validation Functions**: - `ValidateContainerRef` — validates Docker container ID or name - `ValidatePluginName` — validates Unraid plugin names - `ValidateServiceName` — validates system service names - `ValidateSnapshotName` — validates VM snapshot names - **16 New MCP Tools** (9 monitoring, 9 control) with proper annotations ## [2026.02.01] - 2026-02-14 ### Added - **CPU Power Consumption Monitoring** (GitHub Issue #60): - Added Intel RAPL (Running Average Power Limit) support via `/sys/class/powercap` - New `cpu_power_watts` and `dram_power_watts` fields in SystemInfo DTO - Real-time CPU package and DRAM power readings in watts - Multi-socket support — power values summed across all CPU packages - Energy counter wraparound handling for long-running systems - Graceful degradation: fields omitted (null) when RAPL is unavailable (AMD, VMs, older hardware) - New Prometheus gauges: `unraid_cpu_power_watts`, `unraid_dram_power_watts` - Automatically exposed via REST API, WebSocket events, and MCP `get_system_info` tool - Comprehensive test suite with 9 tests covering single/multi-socket, wraparound, edge cases - **MCP Streamable HTTP Transport** (GitHub Issue #59): - Implemented MCP 2025-06-18 Streamable HTTP transport specification - `/mcp` endpoint now supports POST, GET, DELETE, and OPTIONS methods - Session management via `Mcp-Session-Id` header - `MCP-Protocol-Version` header validation (supports 2025-06-18 and 2025-03-26) - POST handles both JSON-RPC requests (with response) and notifications (202 Accepted) - GET opens SSE stream for server-initiated messages - DELETE terminates sessions cleanly (404 for unknown sessions per spec) - Full CORS support with proper header exposure - Fixes "No server info found" error in Cursor - Supports Cursor, Claude Desktop, GitHub Copilot, Codex, Windsurf, and Gemini CLI - Comprehensive test suite with race condition detection (25+ tests) - **MCP STDIO Transport**: - New `mcp-stdio` CLI subcommand for local AI client integration - Uses newline-delimited JSON over stdin/stdout (MCP spec 2025-06-18) - Preferred transport for MCP clients running directly on the Unraid server (zero network overhead, no auth needed) - Starts collectors internally for live data — no dependency on running HTTP daemon - STDIO-safe logging: all logs go to file + stderr (stdout reserved for MCP protocol) - Graceful shutdown on SIGTERM/SIGINT with full collector cleanup - Compatible with Claude Desktop, Cursor, and any MCP client that supports STDIO spawning - Hardened `api.Server.Stop()` to handle nil HTTP server in cache-only mode - Unit tests for STDIO transport initialization, error handling, and context cancellation ### Removed - **Legacy SSE MCP Transport**: Removed the deprecated `/mcp/sse` endpoint and old HTTP transport. All clients should use the Streamable HTTP transport at `/mcp` (spec 2025-06-18). --- ## [2026.02.00] - 2026-01-29 ### Fixed - **Disk Model and Serial Number Population** (GitHub Issue #56): - Fixed `serial_number` and `model` fields in `/api/v1/disks` endpoint returning null - Added `enrichWithModelAndSerial()` function to extract model from sysfs and serial from disk ID - Added `parseModelSerialFromID()` fallback parser for when sysfs is unavailable - Disk ID format discovered: `{model}_{serial}` where spaces in model name are replaced with underscores - Examples: `WUH721816ALE6L4_2CGV0URP`, `WDC_WD100EFAX-68LHPN0_JEKV15MZ`, `SPCC_M.2_PCIe_SSD_A240910N4M051200021` - Serial number validation: alphanumeric, 4-30 characters - Model name restoration: underscores converted back to spaces in output - Handles various disk types: WDC drives, Seagate drives, NVMe SSDs, USB drives - Returns null for unassigned/empty disk slots (correct behavior) - Added comprehensive unit tests: `TestParseModelSerialFromID`, `TestEnrichWithModelAndSerialNoDevice`, `TestEnrichWithModelAndSerialEmptyID` - All tests passing, covering edge cases and various disk ID formats --- ## [2026.01.02] - 2026-01-27 ### Changed - **Go 1.25.0 Upgrade**: - Upgraded from Go 1.24.0 to Go 1.25.0 for improved performance and latest language features - Updated development container, documentation, and CI/CD configurations - All dependencies tested and compatible with Go 1.25 - **golangci-lint v2 Migration**: - Upgraded golangci-lint to v2.8.0 (built with Go 1.25.5) - Migrated configuration to v2 format (.golangci.yml) - Moved gofmt and goimports from linters to formatters section per v2 requirements - Pre-commit hooks updated and re-enabled after migration - All code quality checks passing with zero tolerance policy --- ## [2026.01.01] - 2026-01-22 ### Added - **Prometheus Metrics Endpoint** (#54): - New endpoint `GET /metrics` exposes all Unraid data in Prometheus exposition format - Comprehensive metrics covering: System, Array, Disks, Docker, VMs, UPS, Shares, Network Services, GPU - 40+ metrics with proper labels for multi-dimensional querying - Custom Prometheus registry to isolate Unraid metrics from default Go metrics - Enables native Grafana integration via Prometheus data source - Key metrics include: - `unraid_system_info`, `unraid_cpu_usage_percent`, `unraid_memory_*` - `unraid_array_state`, `unraid_array_*_bytes`, `unraid_parity_*` - `unraid_disk_temperature_celsius`, `unraid_disk_status`, `unraid_disk_smart_status` - `unraid_docker_container_state`, `unraid_docker_containers_*` - `unraid_vm_state`, `unraid_vms_*` - `unraid_ups_*`, `unraid_share_*`, `unraid_service_*`, `unraid_gpu_*` - **Network Services Status API**: - New endpoint `GET /api/v1/settings/network-services` returns comprehensive status of all Unraid network services - Monitors 13 network services: SMB, NFS, AFP, FTP, SSH, Telnet, Avahi, NetBIOS, WSD, WireGuard, UPnP, NTP, Syslog - Each service includes: `name`, `enabled`, `running`, `port`, `description` - Summary counts: `total_services`, `enabled_services`, `running_services` - Parses configuration from `var.ini`, `ident.cfg`, and `tips.and.tweaks.cfg` - Runtime status via process detection in `/proc` - Useful for monitoring dashboards and home automation integrations - **Global Disk Temperature Thresholds API** (#45): - New endpoint `GET /api/v1/settings/disk-thresholds` returns system-wide temperature warning/critical thresholds - Includes separate thresholds for HDDs (hot/max) and SSDs (hotssd/maxssd) from `dynamix.cfg` - Useful for monitoring integrations (Grafana, Home Assistant) that need threshold context - **Per-Disk Temperature Threshold Overrides** (#46): - Extended `DiskInfo` DTO with `temp_warning` and `temp_critical` fields - Parses per-disk overrides from individual disk `.cfg` files - Returns `null` when using global defaults, integer when overridden - **Parity Check Schedule API** (#47): - New endpoint `GET /api/v1/array/parity-check/schedule` returns complete parity schedule configuration - Includes: enabled status, frequency (daily/weekly/monthly/custom), scheduled day/time - Parses from `/boot/config/plugins/dynamix/parity-checks.cron` - **Mover Schedule & Status API** (#48): - New endpoint `GET /api/v1/settings/mover` returns mover configuration and status - Includes: schedule (cron-style), enabled status, whether currently running - Source/destination thresholds, action on share fill, and current operation status - **Docker & VM Service Status API** (#49): - New endpoint `GET /api/v1/settings/services` returns enabled/disabled status - `docker_enabled`: whether Docker service is enabled in Unraid settings - `vm_enabled`: whether VM Manager (libvirt) is enabled - **OS Update Availability API** (#50): - New endpoint `GET /api/v1/updates` returns Unraid OS and plugin update status - Includes: `os_update_available`, `current_version`, `available_version` - Plugin update counts: `plugin_updates_count`, `plugins_with_updates` array - **USB Flash Drive Health API** (#51): - New endpoint `GET /api/v1/system/flash` returns flash boot drive health - Includes: `device`, `total_bytes`, `used_bytes`, `free_bytes`, `used_percent` - `mount_point` and `filesystem` type for diagnostics - **Installed Plugins List API** (#52): - New endpoint `GET /api/v1/plugins` returns all installed plugins with metadata - Each plugin includes: `name`, `version`, `author`, `icon`, `support_url` - `update_available` and `update_version` fields for upgrade awareness - **Share Cache Pool Settings** (#53): - Extended `ShareInfo` DTO with cache pool configuration fields - `cache_pool`: primary cache pool name (or empty for "no") - `cache_pool2`: secondary cache pool for prefer destinations - `mover_action`: computed action ("no_cache", "cache_only", "cache_to_array", "array_to_cache", "cache_prefer") - **Pre-commit Hooks for Code Quality**: - Comprehensive pre-commit configuration with zero tolerance for linting warnings and errors - Automatic code formatting (gofmt, goimports) - Static analysis (go vet, golangci-lint) - Security scanning (gosec, govulncheck) - Secret detection (detect-secrets) - Unit test execution with race detection - Custom checks: VERSION format validation, CHANGELOG reminder, debug print detection - GitHub Actions workflow for CI enforcement - Setup script: `scripts/setup-pre-commit.sh` - Makefile targets: `pre-commit-install`, `pre-commit-run`, `lint`, `security-check` - **Model Context Protocol (MCP) Support** - AI Agent Integration: - New `/mcp` endpoint enables AI agents (Claude, GPT, etc.) to interact with Unraid - Full MCP protocol implementation using mcp-golang v0.16.0 - **18 Monitoring Tools**: system info, array status, disk health, containers, VMs, UPS, GPU, network, notifications, ZFS - **7 Control Tools** (with confirmation for destructive actions): container/VM actions, array control, parity check, reboot/shutdown - **5 MCP Resources** for real-time data access - **3 MCP Prompts** for guided AI interactions - **OpenAPI/Swagger Documentation**: - Interactive API documentation available at `/swagger/` - Full OpenAPI 2.0 specification with 76+ documented endpoints - Auto-generated from code annotations using swaggo/swag ### Fixed - **Parity Check History Null Records (Issue #44)**: - Fixed parity history endpoint returning `null` for records when parity check history exists - Rewrote `parseLine()` to correctly parse the actual Unraid parity log format - Fixed JSON marshaling issue: empty records now return `[]` instead of `null` - Added multi-format support for backward compatibility with older Unraid versions: - 5-field legacy format (pre-2022 Unraid versions) - 7-field current format (2022-present) - 10-field extended format (with parity check tuning plugin) - Added `parseSpeed()` helper to handle both numeric bytes/sec and human-readable "XX.X MB/s" formats - Added comprehensive tests for all parity log format variations - **Log File Accumulation** - Fixed issue where log files were accumulating and consuming excessive disk space (80MB+): - Changed lumberjack `MaxBackups` from 0 to 1 (0 means "keep all", not "keep none") - Changed lumberjack `MaxAge` from 0 to 1 day - Added `cleanupOldLogs()` function that runs on startup to remove old `.gz` backup files - Log rotation now properly limits to 1 backup file maximum - **Dark Theme Support** - Fixed plugin UI not respecting Unraid's dark theme: - Replaced hardcoded CSS colors with Unraid CSS variables (`var(--line-color)`, `var(--text-secondary)`, `var(--input-background)`, etc.) - Status badges, tables, and form elements now properly adapt to light/dark themes - Uses `color: inherit` for text to match theme colors - **Parity Check Status Detection (Issue #41)**: - Fixed parity check status not detecting "paused" state - now correctly parses `mdResyncDt` field (0 = paused, >0 = running) - Fixed parity check progress percentage showing 0 - now calculates from `mdResyncPos / mdResyncSize * 100` - Added support for detecting clearing and reconstructing operations via `sbSyncAction` field - Added debug logging for parity check operations to aid troubleshooting - Status values: `""` (idle), `"paused"`, `"running"`, `"clearing"`, `"reconstructing"` - **Disk Temperature Reporting**: - Improved handling of temperature value `"*"` which indicates spun-down disk - Temperature 0 is now documented expected behavior for standby disks (to avoid spinning up disks for temperature checks) - Enhanced debug logging for temperature parsing - **Power Consumption Optimization** (Issue #8): - Increased default collection intervals to reduce CPU wakeups - System: 5s → 15s, Array: 10s → 30s, Docker: 10s → 30s - VM: 10s → 30s, UPS: 10s → 60s, GPU: 10s → 60s - Optimized Docker stats to batch all containers in single command - Reduced intel_gpu_top timeout (5s → 2s) and samples (2 → 1) - Expected power savings: 15-20W on affected systems ### Changed - **Default Log Level** - Changed default log level from "warning" to "info" for better visibility - **Removed Log Level UI Option** - Log level is now set via CLI only (`--log-level` flag), removed from plugin settings page - **Industry-Standard Default Intervals**: - Updated defaults to follow industry monitoring standards (Zabbix, Prometheus, Datadog) - Disk Health: 30s → 300s (5 min) - SMART data rarely changes - ZFS Pools: 30s → 300s (5 min) - pool health rarely changes - Array Status: 30s → 60s - array state changes infrequently - Hardware Info: 300s → 600s (10 min) - static hardware info - VM Monitoring: 30s → 60s - VMs typically have stable state - Network: 30s → 60s - interface config rarely changes - Registration: 300s → 600s (10 min) - license info is static - **Start Script**: Fixed environment variable passing to Go binary through sudo - **Low Power Mode** (`--low-power-mode` or `UNRAID_LOW_POWER=true`): - New option for resource-constrained/older hardware (e.g., HP N40L with AMD Turion) - Multiplies all collection intervals by 4x when enabled - Reduces CPU wake-ups and allows deeper C-states - Ideal for users experiencing high CPU load from the plugin - **Runtime Collector Management API** (#35): - New endpoint `POST /api/v1/collectors/{name}/enable` - Enable a collector at runtime - New endpoint `POST /api/v1/collectors/{name}/disable` - Disable a collector at runtime - New endpoint `PATCH /api/v1/collectors/{name}/interval` - Update collection interval - New endpoint `GET /api/v1/collectors/{name}` - Get status of a single collector - Enable/disable collectors without restarting the agent - Dynamic interval updates (5-3600 seconds) - System collector protected from being disabled (always required) - WebSocket broadcast for collector state changes (`collector_state_change` event) - Full idempotent operations (enable already enabled = no-op) - Enhanced `/api/v1/collectors/status` with real-time data: - `last_run` timestamp for each collector - `error_count` tracking - `required` flag to indicate if collector can be disabled - **Network Access URLs Endpoint** (`GET /api/v1/network/access-urls`) (#19): - New endpoint to get all methods to access the Unraid server - Returns LAN IPv4 addresses from all network interfaces - Includes mDNS hostname (hostname.local) for Bonjour/Avahi discovery - Detects and includes WireGuard VPN interface addresses - Retrieves public WAN IP (if accessible) - Lists global IPv6 addresses for dual-stack access - URL types: `lan`, `mdns`, `wireguard`, `wan`, `ipv6`, `other` - Useful for dashboards, mobile apps, and connection discovery ### Performance - **Docker SDK Collector (NEW)** - Massive performance improvement: - Replaced CLI-based Docker collector with Docker SDK (socket API) - Uses `/var/run/docker.sock` directly instead of spawning `docker` processes - **~530x faster**: Container list from 5.8s → 10-15ms - **Total docker collection: 15-43ms** (was 8.8 seconds!) - Eliminates process spawning overhead on resource-constrained systems - Still reads memory stats from cgroup v2 filesystem for optimal performance - **VM Libvirt Collector (NEW)** - Native libvirt API integration: - Replaced CLI-based VM collector with libvirt Go bindings - Uses direct RPC to libvirt daemon instead of `virsh` commands - **~100-200x faster**: VM list from 1-2s → 6-7ms - Collects CPU, memory, disk I/O, and network I/O stats efficiently - Graceful fallback if libvirt is not available - **Docker Collector Optimizations** (Community feedback: HP N40L performance issue): - **Batched docker inspect calls**: Reduced from N separate calls to 1 batched call (3.5x faster) - **Skip inspect for stopped containers**: Only running containers get detailed inspection - Overall docker collection cycle reduced from ~8.8s to ~5.9s on test server - Significantly reduces CPU spikes on older hardware - **NUT (Network UPS Tools) Support** (`GET /api/v1/nut`): - Full support for the [NUT-unRAID plugin](https://github.com/desertwitch/NUT-unRAID) - New dedicated `/api/v1/nut` endpoint with comprehensive UPS data - Detects NUT plugin installation and running state - Returns detailed configuration from nut-dw.cfg - Lists all configured UPS devices - Provides detailed UPS status including: - Battery charge, voltage, runtime, type, status - Input/output voltage, frequency, current - Load percentage and real/apparent power - Device identification (manufacturer, model, serial) - Driver information and raw variables - Human-readable status text conversion (OL → "Online", OB → "On Battery", etc.) - WebSocket broadcast support for real-time NUT updates - **Improved UPS Collector NUT Detection**: - Fixed `upsc` command to properly use `@localhost` suffix - UPS endpoint (`/api/v1/ups`) now correctly falls back to NUT when apcupsd unavailable - Both `/api/v1/ups` (basic) and `/api/v1/nut` (detailed) work simultaneously - **Collectors Status API Endpoint** (`GET /api/v1/collectors/status`): - New endpoint to view status of all 14 collectors - Shows enabled/disabled state for each collector - Shows configured interval (in seconds) for each collector - Shows running status ("running" or "disabled") - Provides summary counts: total, enabled_count, disabled_count - Useful for monitoring and debugging collector configuration - **Disable Collectors Feature**: - Added "Disabled" option to all collection interval dropdowns - Setting interval to 0 completely stops the collector (no CPU/memory usage) - Useful for disabling collectors for hardware you don't have (GPU, UPS, ZFS) - Disabled collectors shown with red border highlight in UI - Backend logs which collectors are disabled at startup - **Environment Variable to Disable Collectors** (`UNRAID_DISABLE_COLLECTORS`): - New environment variable for disabling collectors without UI - Comma-separated list of collector names (e.g., `UNRAID_DISABLE_COLLECTORS='gpu,ups,zfs'`) - Validates collector names and warns about unknown names - System collector cannot be disabled (always required) - Ideal for Docker deployments or automated setups - **CLI Flag to Disable Collectors** (`--disable-collectors`): - New command-line flag for disabling collectors at startup - Usage: `--disable-collectors=gpu,ups,zfs` - Same validation as environment variable - Both env var and CLI flag work (CLI flag populates from env var) - **Extended Collection Intervals (up to 24 hours)**: - All collectors now support intervals from 5 seconds to 24 hours (86400 seconds) - New interval options: 1 hour, 2 hours, 4 hours, 6 hours, 12 hours, 24 hours - Ideal for static data that rarely changes (hardware info, registration/license) - Reduces power consumption significantly for infrequently-changing data - **Web UI for Collection Intervals** (Issue #8): - New settings page accessible from Unraid Settings → Unraid Management Agent - Dropdown menus with predefined interval options (5 seconds to 30 minutes) - Organized into logical sections: System Monitoring, Containers & VMs, Hardware, Storage, Other - Human-readable labels (e.g., "30 seconds", "1 minute", "5 minutes") - Power consumption warning explaining impact of faster intervals - Automatic service restart when clicking Apply - No need to manually edit config files - **Configurable Collection Intervals**: - All 14 collection intervals now configurable via UI or config file - Environment variables properly passed to Go binary on service start - Config file persists settings across reboots at `/boot/config/plugins/unraid-management-agent/config.cfg` ### Removed --- ## [2025.12.0] - 2025-12-18 ### Fixed - **Array Disk Counts Inverted** (Issue #30): - Fixed `num_data_disks` and `num_parity_disks` being swapped in `/api/v1/array` endpoint - Removed incorrect use of `mdNumDisabled` field (disabled disks) for data disk count - Data disks now correctly calculated as: `mdNumDisks - active_parity_count` - Parity disk count now only includes active parity disks (excludes disabled/missing) - Disabled parity disks (DISK_NP_DSBL, DISK_NP, DISK_DSBL) are no longer counted - Affects Home Assistant integration and other API clients relying on accurate disk counts --- ## [2025.11.26] - 2025-11-28 ### Added - **Enhanced Log API** (Issue #28): - Expanded `commonLogPaths` from 4 to 30+ common Unraid log file paths - New log files include: dmesg, messages, cron, debug, btmp, lastlog, wtmp, graphql-api.log, unraid-api.log, recycle.log, dhcplog, mover.log, apcupsd.events, nohup.out, nginx error/access logs, vfsd.log, smbd.log, nfsd.log, samba logs, and more - New REST endpoint: `GET /api/v1/logs/{filename}` for direct log file access by filename - Added `lines_returned` field to `LogFileContent` DTO for pagination clarity - Added `ValidateLogFilename()` function in `daemon/lib/validation.go` for secure filename validation - Proper path traversal protection (CWE-22) on new `/logs/{filename}` endpoint - Matches Unraid GraphQL API log coverage for parity - **System Reboot and Shutdown API** (Issue #20): - New REST endpoint: `POST /api/v1/system/reboot` to initiate server reboot - New REST endpoint: `POST /api/v1/system/shutdown` to initiate server shutdown - New `SystemController` in `daemon/services/controllers/system.go` - Enables Home Assistant integration for server power management - Graceful shutdown/reboot using standard Linux shutdown command --- ## [2025.11.25] - 2025-11-18 ### Security - **CRITICAL: Fixed 5 CWE-22 Path Traversal Vulnerabilities** (High Severity): - Fixed path traversal vulnerability in notification controller (`daemon/services/controllers/notification.go`) - Added `validateNotificationID()` function to validate notification IDs before file operations - Protected functions: `ArchiveNotification()`, `UnarchiveNotification()`, `DeleteNotification()` - Blocks parent directory references (`..`), absolute paths, and path separators - Enforces `.notify` file extension requirement - Fixed path traversal vulnerabilities in config collector (`daemon/services/collectors/config.go`) - Added `validateShareName()` function to validate share names before config file access - Protected functions: `GetShareConfig()`, `UpdateShareConfig()` - Blocks parent directory references (`..`), absolute paths, and path separators - Enforces 255 character limit for share names - Implemented defense-in-depth validation strategy: - Input validation at function level (not just API level) - Path normalization using `filepath.Clean()` - Path containment verification using `strings.HasPrefix()` - Multiple validation layers for comprehensive protection - Added comprehensive security test coverage: - `daemon/services/controllers/notification_security_test.go` (4 test suites, 27 test cases) - `daemon/services/collectors/config_security_test.go` (3 test suites, 21 test cases) - All tests validate rejection of malicious path traversal attempts - **Impact**: Prevents attackers from reading or writing arbitrary files on the system - **CWE-22**: Improper Limitation of a Pathname to a Restricted Directory - **Affected Versions**: All versions prior to v2025.11.25 - **Recommendation**: All users should upgrade immediately to v2025.11.25 ### Documentation - Added `SECURITY_FIX_PATH_TRAVERSAL.md` with detailed vulnerability analysis and fix documentation --- ## [2025.11.24] - 2025-11-18 ### Added - **ZFS Storage Pool Support** (GitHub Issue #9): - Complete ZFS integration for monitoring ZFS storage pools on Unraid - New REST API endpoints: - `GET /api/v1/zfs/pools` - List all ZFS pools with comprehensive metrics - `GET /api/v1/zfs/pools/{name}` - Get detailed information about a specific pool - `GET /api/v1/zfs/datasets` - List all ZFS datasets (filesystems and volumes) - `GET /api/v1/zfs/snapshots` - List all ZFS snapshots - `GET /api/v1/zfs/arc` - Get ZFS ARC (Adaptive Replacement Cache) statistics - ZFS collector with 30-second collection interval - Real-time WebSocket events for ZFS pool, dataset, snapshot, and ARC stats updates - Comprehensive ZFS data structures: - Pool metrics: size, allocated space, free space, fragmentation, capacity, dedup ratio, compression ratio - Pool health: state, health status, read/write/checksum errors - VDEVs: virtual device information (mirrors, raidz, disks) - Datasets: filesystem and volume information with compression and quota details - Snapshots: point-in-time snapshots with creation time and space usage - ARC statistics: cache hit ratios, size metrics, L2ARC stats - Automatic detection of ZFS availability (gracefully handles systems without ZFS) - Full support for ZFS pool properties: autoexpand, autotrim, readonly, altroot - Scrub/resilver status tracking ### Technical Details - **New Files**: - `daemon/dto/zfs.go`: ZFS data transfer objects (ZFSPool, ZFSVdev, ZFSDevice, ZFSDataset, ZFSSnapshot, ZFSARCStats, ZFSIOStats) - `daemon/services/collectors/zfs.go`: ZFS collector implementation with parsers for zpool/zfs command output - `docs/ZFS_INVESTIGATION_FINDINGS.md`: Complete investigation findings and implementation documentation - **Modified Files**: - `daemon/constants/const.go`: Added ZFS binary paths and collection interval constants - `daemon/services/orchestrator.go`: Integrated ZFS collector into application lifecycle - `daemon/services/api/server.go`: Added ZFS cache fields and event subscriptions - `daemon/services/api/handlers.go`: Implemented ZFS endpoint handlers - **ZFS Data Sources**: - `/usr/sbin/zpool list -Hp`: Pool metrics (parseable format) - `/usr/sbin/zpool status -v`: Pool status, vdev tree, error counters - `/usr/sbin/zpool get all`: Pool properties - `/usr/sbin/zfs list -Hp`: Dataset information - `/proc/spl/kstat/zfs/arcstats`: ARC cache statistics - **Testing**: - Validated on Unraid 7.2.0 with ZFS 2.3.4-1 - Tested with "garbage" pool (222GB, single disk, ONLINE) - All endpoints returning correct data - ARC hit ratio: 99.89% (53,172 hits, 58 misses) --- ## [2025.11.23] - 2025-11-17 ### Changed - **Dependency Updates**: - Updated `github.com/alecthomas/kong` from v0.9.0 to v1.13.0 - Updated `golang.org/x/sys` from v0.13.0 to v0.38.0 - Upgraded Go language version from 1.23 to 1.24.0 (required by golang.org/x/sys v0.38.0) - Migrated from deprecated `github.com/vaughan0/go-ini` (last updated 2013) to actively maintained `gopkg.in/ini.v1` v1.67.0 - All dependencies now use modern, actively maintained libraries - **Code Quality Improvements**: - Fixed all pre-existing linting issues (now 0 errors, 0 warnings) - Converted if-else chains to switch statements for better readability (gocritic) - Reduced cyclomatic complexity by extracting helper functions (gocyclo) - Renamed `daemon/common` package to `daemon/constants` for better clarity (revive) - Improved code maintainability and adherence to Go best practices ### Technical Details - **Linting Fixes**: - `daemon/lib/dmidecode.go`: Converted cache level parsing to switch statement - `daemon/lib/ethtool.go`: Extracted `parseEthtoolKeyValue()` helper (complexity 34 → 18) - `daemon/services/collectors/disk.go`: Extracted `parseDisksINI()`, `parseDiskKeyValue()`, `enrichDisks()` helpers (complexity 32 → 12) - `daemon/services/collectors/disk.go`: Converted disk role determination to switch statement - `daemon/services/collectors/gpu.go`: Converted Intel GPU error handling to switch statement - **INI Library Migration**: - Updated `daemon/lib/parser.go`: Refactored `ParseINIFile()` to use new API - Updated `daemon/services/collectors/array.go`: Migrated `collectArrayStatus()` and `countParityDisks()` - Updated `daemon/services/collectors/registration.go`: Migrated `collectRegistration()` - API changes: `ini.LoadFile()` → `ini.Load()`, `file.Get()` → `section.HasKey()` + `section.Key().String()` All tests pass successfully. Builds verified for local and release targets. --- ## [2025.11.22] - 2025-11-17 ### Added - **Management Agent Version Field** (Issue #26): - Added `agent_version` field to `/api/v1/system` endpoint - Returns the Unraid Management Agent plugin version (e.g., "2025.11.22") - Complements the `version` field which returns the Unraid OS version - Enables API clients to: - Implement agent-specific compatibility checks - Detect features based on agent capabilities - Display complete version information in diagnostics - Track agent updates independently from OS updates - Improves Home Assistant integration and other API clients - Version is automatically populated from the VERSION file during build --- ## [2025.11.21] - 2025-11-17 ### Fixed - **Motherboard Temperature API** (Issue #24): - Fixed motherboard temperature returning 0 instead of actual value - Improved sensor parser to capture sensor labels (e.g., "MB Temp") from `sensors -u` output - Updated temperature matching logic to correctly identify motherboard temperature sensor - Motherboard temperature now displays actual readings (e.g., 45°C) instead of 0°C - Affects Home Assistant integration and other API clients relying on temperature data - **Unraid OS Version Field** (Issue #25): - Fixed system API returning empty string for `version` field - Added `getUnraidVersion()` function to read Unraid OS version from `/etc/unraid-version` - Fallback to `/var/local/emhttp/var.ini` if primary version file is unavailable - Version field now correctly displays Unraid OS version (e.g., "7.2.0") - Improves device information display in Home Assistant and other integrations - Enables version-specific feature detection and compatibility checks --- ## [2025.11.20] - 2025-11-16 ### Fixed - **VM Endpoint ID Field** (Issue #22): - Fixed VM endpoint returning empty string for `id` field - Changed from using `virsh domid` (runtime ID) to `virsh domuuid` (persistent UUID) - VM IDs are now stable, unique identifiers that work for all VM states (running, shut off, paused) - Provides consistent identification for API clients and automation systems - Falls back to VM name if UUID is not available - **Array Parity Status** (Issue #21): - Fixed incorrect parity status reporting (`parity_valid: false` when parity is actually valid) - Fixed incorrect parity disk count (`num_parity_disks: 0` when parity disks exist) - Now correctly counts parity disks from `disks.ini` (field `type="Parity"`) - Improved parity validity logic to check `sbSynced` timestamp and `sbSyncErrs` count - Parity is marked valid only when: - At least one parity disk exists - Parity has been synced (sbSynced is non-zero timestamp) - No parity errors exist (sbSyncErrs is 0) ### Technical Details - **VM Collector**: Updated `getVMID()` to use `virsh domuuid` for stable UUID-based identification - **Array Collector**: Added `countParityDisks()` function to parse disks.ini and count parity disks - **Parity Logic**: Improved validation to check timestamp-based sync status instead of yes/no flag - **Compatibility**: Both fixes maintain backward compatibility with existing API clients --- ## [2025.11.19] - 2025-11-16 ### Fixed - **Code Quality Improvements**: - Resolved all critical linting warnings across the entire codebase (93% reduction from 302 to 22 warnings) - Added comprehensive godoc comments to all exported types, functions, and methods - Fixed all gosec security warnings with proper justifications - Fixed revive warnings (indent-error-flow, empty-block) - Fixed staticcheck warnings (unnecessary nil checks) - Fixed unconvert warnings (unnecessary type conversions) - Fixed ineffassign warnings (ineffectual assignments) - Achieved zero tolerance policy compliance for all critical linting issues ### Technical Details - **Files Modified**: 21 files across collectors, controllers, API server, and main package - **Linting Compliance**: Zero critical errors, zero critical warnings - **Documentation**: All exported symbols now have proper godoc comments - **Security**: All gosec warnings properly justified with #nosec comments - **Code Style**: Removed unnecessary else blocks, nil checks, and type conversions --- ## [2025.11.18] - 2025-11-16 ### Added - **Unassigned Devices Plugin Support** (Issue #7): - Complete support for Unassigned Devices plugin integration - New `/api/v1/unassigned` endpoint to list all unassigned devices and remote shares - New `/api/v1/unassigned/devices` endpoint for unassigned devices only - New `/api/v1/unassigned/remote-shares` endpoint for remote shares only - Automatic detection of unassigned disk devices (USB drives, eSATA, internal disks not in array) - Support for remote SMB/NFS shares and ISO file mounts - Per-device information: serial number, model, partitions, mount status, spin state - Per-partition information: label, filesystem, mount point, size, usage, SMB/NFS share status - Automatic filtering of array disks, loop devices, md devices, and zram - Real-time monitoring with 30-second collection interval - WebSocket real-time updates when unassigned devices change ### Technical Details - **New DTOs**: `daemon/dto/unassigned.go` - UnassignedDevice, UnassignedPartition, UnassignedRemoteShare, UnassignedDeviceList structures - **New Collector**: `daemon/services/collectors/unassigned.go` - Unassigned devices collector with lsblk integration - **Device Discovery**: Uses lsblk to enumerate all block devices and filters out array disks - **Array Disk Detection**: Parses `/var/local/emhttp/disks.ini` to identify array disks - **Partition Information**: Collects filesystem type, mount point, size, usage for each partition - **Remote Share Support**: Detects mounted ISO files from `/proc/mounts` - **API Integration**: Added 3 new monitoring endpoints for unassigned devices - **WebSocket Events**: Real-time `unassigned_devices_update` events for connected clients - **Collection Interval**: 30 seconds for device discovery and status updates --- ## [2025.11.17] - 2025-11-16 ### Added - **Unraid Notifications System** (Issue #10): - Complete notification management system with file monitoring - New `/api/v1/notifications` endpoint to list all notifications with overview counts - New `/api/v1/notifications/unread` endpoint for unread notifications only - New `/api/v1/notifications/archive` endpoint for archived notifications only - New `/api/v1/notifications/overview` endpoint for notification counts by importance - New `/api/v1/notifications/{id}` endpoint to get specific notification - Create custom notifications via POST `/api/v1/notifications` - Archive notifications via POST `/api/v1/notifications/{id}/archive` - Unarchive notifications via POST `/api/v1/notifications/{id}/unarchive` - Delete notifications via DELETE `/api/v1/notifications/{id}` - Archive all unread notifications via POST `/api/v1/notifications/archive/all` - Real-time file monitoring with fsnotify for instant notification updates - Automatic notification discovery from `/usr/local/emhttp/state/notifications/` - Support for all importance levels: alert, warning, info - Notification counts by type (unread/archive) and importance level - WebSocket real-time updates when notifications change - Filter notifications by importance level via query parameter ### Technical Details - **New DTOs**: `daemon/dto/notification.go` - Notification, NotificationOverview, NotificationCounts, NotificationList structures - **New Collector**: `daemon/services/collectors/notification.go` - File-based notification collector with fsnotify monitoring - **New Controller**: `daemon/services/controllers/notification.go` - Notification CRUD operations (create, archive, unarchive, delete) - **File Monitoring**: Uses fsnotify to watch notification directories for real-time updates - **Collection Interval**: 15 seconds (with instant updates via file watcher) - **Notification Format**: Parses Unraid .notify files with key-value pairs - **Archive Support**: Moves notifications between active and archive directories - **Bulk Operations**: Archive all unread notifications at once - **Security**: Proper file permissions (0644 for files, 0755 for directories) - **Error Handling**: Graceful handling of missing directories, parse errors, and file operations --- ## [2025.11.16] - 2025-11-16 ### Added - **System Log File Access API** (Issue #15): - New `/api/v1/logs` endpoint to list all available log files - Log content retrieval with pagination support via query parameters - Tail behavior: `?path=/var/log/syslog&lines=100` returns last 100 lines - Range retrieval: `?path=/var/log/syslog&start=500&lines=100` returns lines 500-600 - Automatic discovery of common Unraid log files (syslog, docker.log, libvirtd.log, agent log) - Plugin log file discovery from `/boot/config/plugins/*/logs/*.log` - Directory traversal protection for secure log file access - Returns log metadata: name, path, size, modified timestamp - Returns log content: full content string, line array, total lines, start/end line numbers ### Technical Details - **New DTOs**: `daemon/dto/logs.go` - LogFile and LogFileContent structures - **New API Module**: `daemon/services/api/logs.go` - Log listing and content retrieval logic - **Security**: Path validation prevents directory traversal attacks - **Pagination**: Supports both tail behavior (last N lines) and range retrieval (start + lines) - **Common Logs**: Automatically discovers syslog, Docker, libvirt, and plugin logs - **Error Handling**: Graceful handling of missing files, permission errors, and invalid paths --- ## [2025.11.15] - 2025-11-16 ### Added - **Registration/License Information API** (Issue #18): - New `/api/v1/registration` endpoint to retrieve Unraid license and registration information - Returns license type (trial, basic, plus, pro, lifetime), state (valid, expired, invalid, trial), expiration dates, server name, and registration GUID - Automatically determines license state based on expiration date and license type - Supports all Unraid license types including trial, basic, plus, pro, and lifetime/unleashed licenses - Real-time updates via WebSocket when registration information changes - Graceful handling of missing or invalid registration data ### Technical Details - **New DTO**: `daemon/dto/registration.go` - Registration data structure with license type, state, expiration, server name, and GUID - **New Collector**: `daemon/services/collectors/registration.go` - Parses `/var/local/emhttp/var.ini` for registration data - **API Integration**: Added registration cache, handler, route, and event subscription to API server - **Event Bus**: Registration collector publishes `registration_update` events to the PubSub hub - **Collection Interval**: Uses hardware collection interval (60 seconds) for registration data updates --- ## [2025.11.14] - 2025-11-16 ### Added - **Enhanced GPU Monitoring and Multi-GPU Support** (Issue #8 - High Priority Items): - **GPU Identification Fields**: Added `Index`, `PCIID`, `Vendor`, and `UUID` fields to GPUMetrics DTO - `Index`: GPU index for multi-GPU systems (0-based) - `PCIID`: PCI bus ID (e.g., "0000:01:00.0") - `Vendor`: GPU vendor ("nvidia", "intel", "amd") - `UUID`: Device UUID (NVIDIA only) - **Fan Speed Monitoring**: - NVIDIA: `FanSpeed` field (percentage, 0-100) - AMD: `FanRPM` and `FanMaxRPM` fields (discrete GPUs only) - **AMD GPU Detection Improvements**: - Switched from `rocm-smi` to `radeontop` for broader AMD GPU compatibility - Now supports consumer Radeon GPUs (RX 5000/6000/7000 series) - Fallback to `rocm-smi` for datacenter GPUs (Instinct series) - AMD GPU temperature and fan speed read from sysfs - **Multiple Intel GPU Detection**: - Fixed bug where only first Intel GPU was detected - Now detects all Intel GPUs (iGPU + discrete Arc GPUs) - Each Intel GPU gets unique index and PCI ID - **NVIDIA Enhancements**: - Added UUID field for unique GPU identification - Added fan speed monitoring (percentage) - Added PCI bus ID extraction ### Changed - **GPU Collector**: Complete refactor to support multi-GPU systems - Intel GPU collector now detects ALL Intel GPUs (removed `break` statement) - AMD GPU collector uses `radeontop` by default, `rocm-smi` as fallback - NVIDIA GPU collector queries additional fields (UUID, PCI ID, fan speed) - All GPU collectors now populate vendor, index, and PCI ID fields - **GPU API Response**: Already returns array of GPUMetrics (no breaking change) - **GPU Detection Order**: Intel → NVIDIA → AMD (unchanged) ### Technical Details - **Intel Multi-GPU**: Collects metrics for each detected Intel GPU separately - Note: `intel_gpu_top` limitation - reports only first GPU's metrics - Multiple GPUs detected via lspci, but metrics may be from primary GPU - **AMD radeontop**: Parses dump mode output for GPU/VRAM utilization - Temperature read from `/sys/class/drm/card*/device/hwmon/hwmon*/temp1_input` - Fan speed read from `/sys/class/drm/card*/device/hwmon/hwmon*/fan1_input` - Fan max RPM read from `/sys/class/drm/card*/device/hwmon/hwmon*/fan1_max` - **NVIDIA nvidia-smi**: Extended query to include `pci.bus_id`, `uuid`, `fan.speed` - **Driver Versions**: Extracted from `modinfo` (Intel/AMD) or `nvidia-smi` (NVIDIA) ### Compatibility - **Backward Compatible**: All new fields use `omitempty` JSON tags - **AMD GPU Requirements**: - Consumer GPUs: Requires `radeontop` (install via Nerd Tools or manual) - Datacenter GPUs: Requires `rocm-smi` (ROCm packages) - **Intel GPU Requirements**: `intel_gpu_top` from `igt-gpu-tools` (Unraid 6.12+) - **NVIDIA GPU Requirements**: `nvidia-smi` (included with NVIDIA driver) ### Known Limitations - **Intel Multi-GPU**: `intel_gpu_top` doesn't support per-device metrics - Multiple Intel GPUs detected, but metrics may be from primary GPU only - This is a limitation of `intel_gpu_top` tool, not the agent - **AMD Fan Speed**: Only available on discrete GPUs with fan sensors - Integrated AMD GPUs (APUs) don't have fan sensors - **AMD radeontop**: May require manual installation on some systems --- ## [2025.11.13] - 2025-11-16 ### Added - **Enhanced Share List API** (Issue #6): Eliminated N+1 query problem for share information - Share list endpoint `/api/v1/shares` now includes configuration details directly - New fields in ShareInfo DTO: - `comment` - Share comment/description from config - `smb_export` - Boolean indicating if share is exported via SMB - `nfs_export` - Boolean indicating if share is exported via NFS - `storage` - Storage location: "cache", "array", "cache+array", or "unknown" - `use_cache` - Cache usage setting: "yes", "no", "only", "prefer" - `security` - Security setting: "public", "private", "secure" - Share collector automatically enriches shares with config data - SMB export detection based on security settings and export field - NFS export detection based on export field - Storage location determined from UseCache setting - All new fields use `omitempty` for backward compatibility - Single API call now provides complete share information matching Unraid UI - Graceful error handling: shares without config files return basic info only ### Changed - Share collector now reads individual share config files during collection - Share enrichment happens automatically for all shares in the list ### Performance - Share collection remains at 60-second interval - Config file reads are lightweight and fast - No performance impact from enrichment process --- ## [2025.11.12] - 2025-11-16 ### Added - **Hardware Information API** (Issue #5): Comprehensive hardware details via dmidecode and ethtool - New `/api/v1/hardware/*` endpoints exposing detailed hardware information - `/api/v1/hardware/full` - Complete hardware information - `/api/v1/hardware/bios` - BIOS information (vendor, version, release date, characteristics) - `/api/v1/hardware/baseboard` - Motherboard information (manufacturer, product name, version, serial number) - `/api/v1/hardware/cpu` - CPU hardware details (socket, manufacturer, family, max speed, core/thread count, voltage) - `/api/v1/hardware/cache` - CPU cache information (L1/L2/L3 cache levels, size, type, associativity) - `/api/v1/hardware/memory-array` - Memory array information (location, max capacity, error correction, number of devices) - `/api/v1/hardware/memory-devices` - Individual memory module details (size, speed, manufacturer, part number, type) - Hardware collector runs every 5 minutes (hardware information is static) - All hardware data is cached and broadcast via WebSocket for real-time updates - **Enhanced System Information**: - `HVMEnabled` - Hardware virtualization support (Intel VT-x/AMD-V detection via /proc/cpuinfo) - `IOMMUEnabled` - IOMMU support detection (kernel command line and /sys/class/iommu/) - `OpenSSLVersion` - OpenSSL version information - `KernelVersion` - Linux kernel version - `ParityCheckSpeed` - Current parity check speed from var.ini - **Enhanced Network Information** via ethtool: - `SupportedPorts` - Supported port types (TP, AUI, MII, Fibre, etc.) - `SupportedLinkModes` - Supported link speeds and modes - `SupportedPauseFrame` - Pause frame support - `SupportsAutoNeg` - Auto-negotiation support - `SupportedFECModes` - Forward Error Correction modes - `AdvertisedLinkModes` - Advertised link speeds and modes - `AdvertisedPauseFrame` - Advertised pause frame use - `AdvertisedAutoNeg` - Advertised auto-negotiation - `AdvertisedFECModes` - Advertised FEC modes - `Duplex` - Duplex mode (Full/Half) - `AutoNegotiation` - Auto-negotiation status (on/off) - `Port` - Port type (Twisted Pair, Fibre, etc.) - `PHYAD` - PHY address - `Transceiver` - Transceiver type (internal/external) - `MDIX` - MDI-X status (on/off/Unknown) - `SupportsWakeOn` - Supported Wake-on-LAN modes - `WakeOn` - Current Wake-on-LAN setting - `MessageLevel` - Driver message level - `LinkDetected` - Link detection status - `MTU` - Maximum Transmission Unit - **New Libraries**: - `daemon/lib/dmidecode.go` - Parser for dmidecode output (SMBIOS/DMI types 0, 2, 4, 7, 16, 17) - `daemon/lib/ethtool.go` - Parser for ethtool output with comprehensive network interface details - **New DTOs**: - `HardwareInfo` - Container for all hardware information - `BIOSInfo` - BIOS/UEFI information - `BaseboardInfo` - Motherboard/baseboard information - `CPUHardwareInfo` - CPU hardware specifications - `CPUCacheInfo` - CPU cache level information - `MemoryArrayInfo` - Memory array/controller information - `MemoryDeviceInfo` - Individual memory module information ### Changed - **System Collector**: Enhanced with virtualization and additional system information - Added `isHVMEnabled()` method to detect hardware virtualization support - Added `isIOMMUEnabled()` method to detect IOMMU support - Added `getOpenSSLVersion()` method to retrieve OpenSSL version - Added `getKernelVersion()` method to retrieve kernel version - Added `getParityCheckSpeed()` method to parse parity check speed from var.ini - **Network Collector**: Enhanced with ethtool integration - Added `enrichWithEthtool()` method to populate network interface details - Network information now includes comprehensive ethtool data when available - Gracefully handles cases where ethtool is not available or fails - **Orchestrator**: Updated to manage hardware collector - Increased collector count from 9 to 10 - Hardware collector initialized and started with 5-minute interval - **API Server**: Updated to cache and serve hardware information - Added `hardwareCache` field to Server struct - Subscribed to `hardware_update` events - Hardware events broadcast to WebSocket clients --- ## [2025.11.11] - 2025-11-08 ### Fixed - **VM CPU Percentage Tracking**: Implemented proper CPU percentage calculation for VMs - Added historical tracking to VM collector using `cpuStats` struct with mutex protection - Guest CPU % now calculated from `virsh domstats` CPU time deltas over time intervals - Host CPU % now calculated from QEMU process CPU usage via `/proc/[pid]/stat` - CPU percentages are calculated as: `(current_time - previous_time) / time_interval / num_vcpus * 100` - Percentages are clamped to valid range [0, 100] to handle edge cases - CPU stats history is automatically cleared when VMs are shut off - First measurement after VM start returns 0% (requires two measurements for delta calculation) - Subsequent measurements return accurate real-time CPU percentages - Host CPU % matches the percentage shown in `ps`/`top` for the QEMU process - Guest CPU % represents the percentage of allocated vCPUs being used inside the guest OS ### Changed - **VM Collector**: Enhanced with CPU tracking infrastructure - Added `cpuStats` struct to store guest CPU time, host CPU time, and timestamp - Added `previousStats` map with mutex for thread-safe historical tracking - Added `getGuestCPUTime()` method using `virsh domstats --cpu-total` - Added `getHostCPUTime()` method reading `/proc/[pid]/stat` - Added `getQEMUProcessPID()` method using `pgrep` to find QEMU process - Added `clearCPUStats()` method to reset tracking when VMs are shut off - Updated `getVMCPUUsage()` to accept `numVCPUs` parameter and calculate real percentages ### Removed - **Placeholder CPU Values**: Removed the "needs historical data" limitation - CPU percentage fields now return real values instead of always returning 0 - Removed placeholder comments about needing historical data implementation --- ## [2025.11.10] - 2025-11-08 ### Added - **Enhanced VM Statistics**: Added comprehensive VM monitoring metrics - Guest CPU usage percentage (placeholder for future implementation with historical data) - Host CPU usage percentage (placeholder for future implementation with historical data) - Memory display in human-readable format (e.g., "1.17 GB / 4.00 GB") - Disk I/O statistics: total read and write bytes across all VM disks - Network I/O statistics: total RX and TX bytes across all VM network interfaces - New DTO fields: `guest_cpu_percent`, `host_cpu_percent`, `memory_display`, `disk_read_bytes`, `disk_write_bytes`, `network_rx_bytes`, `network_tx_bytes` - **Enhanced Docker Container Statistics**: Added comprehensive container monitoring metrics - Container version extracted from image tag - Network mode (e.g., "bridge", "host", "none") - Container IP address - Port mappings in "host_port:container_port" format - Volume mappings with host path, container path, and mode (rw/ro) - Restart policy (e.g., "always", "unless-stopped", "on-failure", "no") - Container uptime in human-readable format (e.g., "2d 5h 30m", "3h 45m", "15m") - Memory display in human-readable format (e.g., "512.00 MB / 2.00 GB") - New DTO fields: `version`, `network_mode`, `ip_address`, `port_mappings`, `volume_mappings`, `restart_policy`, `uptime`, `memory_display` ### Changed - **VM Collector**: Enhanced data collection using additional virsh commands - Added `getVMCPUUsage()` method using `virsh cpu-stats` (returns 0 pending historical data implementation) - Added `getVMDiskIO()` method using `virsh domblklist` and `virsh domblkstat` - Added `getVMNetworkIO()` method using `virsh domiflist` and `virsh domifstat` - Added `formatMemoryDisplay()` helper for human-readable memory formatting - **Docker Collector**: Enhanced data collection using docker inspect - Added `getContainerDetails()` method using `docker inspect` for comprehensive container metadata - Added `formatUptime()` helper for human-readable uptime formatting - Added `formatMemoryDisplay()` helper for human-readable memory formatting - Container details now include network configuration, volume mappings, and restart policies --- ## [2025.11.9] - 2025-11-08 ### Fixed - **VM Collector**: Fixed parsing of VM names containing spaces - Changed from parsing `virsh list --all` column-based output to using `virsh list --all --name` - Added `getVMState()` helper method to get VM state using `virsh domstate ` - Added `getVMID()` helper method to get VM ID using `virsh domid ` - Now correctly handles VM names with spaces, hyphens, underscores, and special characters - Example: "Windows Server 2016" is now correctly parsed instead of being split into "Windows" and "Server 2016 running" - **VM Control API**: Fixed VM control endpoints to work with VM names containing spaces - Updated VM name validation regex to allow spaces: `^[a-zA-Z0-9 _.-]{1,253}$` - Fixed route parameter mismatch: changed VM control routes from `{id}` to `{name}` - VM control endpoints now correctly accept URL-encoded spaces (e.g., `Windows%20Server%202016`) - All VM operations (start, stop, restart, pause, resume, hibernate, force-stop) now work with spaces in VM names --- ## [2025.11.8] - 2025-11-08 ### Added - **User Scripts API**: New REST API endpoints for discovering and executing Unraid User Scripts - GET `/api/v1/user-scripts` - List all available user scripts with metadata - POST `/api/v1/user-scripts/{name}/execute` - Execute a user script with background/wait options - Supports reading script descriptions from the `description` file - Includes path traversal protection and input validation - Returns script metadata: name, description, path, executable status, last modified timestamp - Execution options: `background` (default: true), `wait` (default: false) - Enables automation tools like Home Assistant to remotely execute Unraid maintenance scripts --- ## [2025.11.7] - 2025-11-08 ### Changed - **README.md Simplified**: Reduced README.md to essential information only - Removed detailed feature lists, API endpoints, and support links - Kept only the plugin name heading and brief description - Maintains proper display name format for Plugin Manager - Reduces file size while preserving functionality --- ## [2025.11.6] - 2025-11-08 ### Fixed - **Plugin Display Name**: Plugin now displays as "Unraid Management Agent" in the Unraid Plugin Manager instead of "unraid-management-agent" - Added README.md file to plugin directory with proper display name formatting - Follows Unraid plugin naming conventions used by Community Applications and other established plugins - Settings menu display name remains unchanged (was already correct) - Improves user experience and plugin discoverability in the Plugin Manager --- ## [2025.11.5] - 2025-11-08 ### Added - **USB Flash Drive Detection**: Plugin now detects USB flash drives (including the Unraid boot drive) and skips SMART data collection - Checks device sysfs path to identify USB transport - Detects Unraid boot drive by checking if device is mounted at `/boot` - Avoids unnecessary SMART commands on devices that don't support SMART monitoring - SMART status remains "UNKNOWN" for USB flash drives (consistent with previous behavior) - Adds debug logging to indicate when USB flash drive detection occurs ### Changed - **NVMe-Specific SMART Collection**: Optimized SMART data collection for NVMe drives - NVMe drives are now detected by checking device name pattern (e.g., `nvme0n1`) - NVMe drives skip the `-n standby` flag since they don't support standby mode - Uses `smartctl -H /dev/{device}` for NVMe drives (without `-n standby`) - SATA/SAS drives continue to use `smartctl -n standby -H /dev/{device}` (existing behavior) - Adds debug logging to indicate device type detection (NVMe vs SATA/SAS) - Improves efficiency by avoiding unnecessary standby checks on NVMe drives --- ## [2025.11.4] - 2025-11-08 ### Fixed - **CRITICAL FIX**: Disk spin-down compatibility - Plugin now respects Unraid's disk spin-down settings - Changed SMART data collection to use `smartctl -n standby` flag - Disks in standby mode are no longer woken up for SMART health checks - Previous implementation was preventing disks from spinning down by accessing them every 30 seconds - SMART status is now only collected when disks are already active - Preserves power savings and reduces disk wear for users with spin-down configured - Fixes critical issue where plugin prevented Unraid's disk spin-down functionality from working --- ## [2025.11.3] - 2025-11-08 ### Changed - Improved plugin UI/UX in Unraid interface - Added clickable settings link to plugin icon - Updated settings page title to "Unraid Management Agent" - Added server icon to plugin listing ### Fixed - Settings page URL now uses correct lowercase-with-hyphens format - Changed from `/Settings/UnraidManagementAgent` to `/Settings/unraid-management-agent` - Plugin icon now navigates to the correct settings page URL - **CRITICAL FIX**: SMART health status now correctly retrieved by running `smartctl -H` directly - Previous implementation tried to read from Unraid's cached files which use disk names instead of device names - Cached files also don't include the health status line - Now executes `smartctl -H /dev/{device}` to get actual health status - Fixes issue where all disks showed `smart_status: "UNKNOWN"` (Issue #4) --- ## [2025.11.2] - 2025-11-07 ### Added - Automated CI/CD workflow for releases using GitHub Actions - Automatically builds release package when Git tag is pushed - Calculates MD5 checksum and includes in release notes - Creates GitHub release with .tgz file attached - Extracts release notes from CHANGELOG.md - Supports pre-release detection (alpha, beta, rc versions) ### Fixed - SMART health status now correctly reported in `/api/v1/disks` endpoint (#4) - Parses actual SMART health status from Unraid's cached smartctl output - Returns "PASSED" for healthy disks (SATA/SAS drives) - Returns "PASSED" for healthy NVMe drives (normalizes "OK" to "PASSED") - Returns actual status values like "FAILED" when SMART tests fail - No longer returns "UNKNOWN" for all disks when SMART data is available --- ## [2025.11.1] - 2025-11-07 ### Added - Docker vDisk usage monitoring in `/api/v1/disks` endpoint (#2) - Automatically detects Docker vDisk at `/var/lib/docker` mount point - Reports size, used, free bytes, and usage percentage - Identifies vDisk file path (e.g., `/mnt/user/system/docker/docker.vdisk`) - Includes filesystem type detection - Assigned role `docker_vdisk` for easy filtering - Enables monitoring of Docker storage capacity for alerts - Log filesystem usage monitoring in `/api/v1/disks` endpoint (#3) - Automatically detects log filesystem at `/var/log` mount point - Reports size, used, free bytes, and usage percentage - Identifies device name (e.g., `tmpfs` for RAM-based log storage) - Includes filesystem type detection (tmpfs, ext4, xfs, etc.) - Assigned role `log` for easy filtering - Enables monitoring of log storage capacity to prevent system failures - Critical for tmpfs-based log filesystems that can fill up and cause issues ### Fixed - UPS API endpoint now returns actual UPS model name instead of hostname (#1) --- ## [2025.11.0] - 2025-11-03 ### Added #### Enhanced System Information Collector - **CPU Model Detection**: Automatic detection of CPU model, cores, threads, and frequency from `/proc/cpuinfo` - **BIOS Information**: Server model, BIOS version, and BIOS release date via `dmidecode` - **Per-Core CPU Usage**: Individual CPU core usage monitoring with `cpu_per_core_usage` field - **Server Model Identification**: Hardware model detection for better system identification #### Detailed Disk Metrics - **I/O Statistics**: Read/write operations and bytes per disk from `/sys/block/{device}/stat` - `read_ops` - Total read operations - `read_bytes` - Total bytes read - `write_ops` - Total write operations - `write_bytes` - Total bytes written - `io_utilization_percent` - Disk I/O utilization percentage - **Disk Spin State Detection**: Enhanced spin state detection (active, standby, unknown) - **Per-Disk Performance Metrics**: Real-time performance monitoring for each disk ### Changed #### Documentation Updates - **README.md Roadmap Reorganization**: - Added "Recently Implemented ✅" section to highlight completed features - Moved Enhanced System Info Collector and Detailed Disk Metrics from planned to implemented - Updated "Planned Enhancements" to only include outstanding features - Added detailed sub-items for each implemented feature - **Third-Party Plugin Notice**: Added prominent disclaimer distinguishing this plugin from official Unraid API - **System Compatibility Section**: Added hardware compatibility notice and tested configuration details - **Contributing Guidelines**: Expanded contribution workflow for hardware compatibility fixes - **Version References**: Updated from Unraid 6.x to 7.x throughout documentation #### Configuration Management - **Log Rotation**: Implemented automatic log rotation with 5 MB max file size (using lumberjack.v2) - **Log Level Support**: Added configurable log levels (DEBUG, INFO, WARNING, ERROR) with `--log-level` CLI flag - **Default Log Level**: Set to WARNING for production to minimize disk usage - **Configuration File Management**: Improved config file creation and synchronization - **Auto-Start Behavior**: Service now always starts automatically when Unraid array starts (removed toggle option) ### Fixed - **Configuration Synchronization**: Fixed LOG_LEVEL not being read from config file - **Start Script**: Now properly creates config directory and default config file - **Deployment Script**: Updated to use start script instead of bypassing configuration ### Testing - **Test Suite**: All 66 tests passing across 3 packages (100% pass rate) - **Deployment Verification**: Successfully deployed and verified on Unraid 7.x server --- ## [2025.10.03] - Initial Release ### Added #### Phase 1 & 2 API Enhancements **Phase 1.1: Array Control Operations** - `POST /api/v1/array/start` - Start the Unraid array - `POST /api/v1/array/stop` - Stop the Unraid array - `POST /api/v1/array/parity-check/start` - Start parity check (with correcting option) - `POST /api/v1/array/parity-check/stop` - Stop parity check - `POST /api/v1/array/parity-check/pause` - Pause parity check - `POST /api/v1/array/parity-check/resume` - Resume parity check - `GET /api/v1/array/parity-check/history` - Get parity check history from log file - New `ParityCollector` to parse `/boot/config/parity-checks.log` - New `ParityCheckRecord` and `ParityCheckHistory` DTOs **Phase 1.2: Single Resource Endpoints** - `GET /api/v1/disks/{id}` - Get single disk by ID, device, or name - `GET /api/v1/docker/{id}` - Get single container by ID or name - `GET /api/v1/vm/{id}` - Get single VM by ID or name - Support for multiple identifier types (ID, name, device) - 404 error handling for missing resources **Phase 1.3: Enhanced Disk Details** - Added `serial_number` field to DiskInfo DTO - Added `model` field to DiskInfo DTO - Added `role` field to DiskInfo DTO (parity, parity2, data, cache, pool) - Added `spin_state` field to DiskInfo DTO (active, standby, unknown) - Automatic role detection based on disk name/ID - Spin state detection based on temperature - SMART data extraction for serial number and model **Phase 2.1: Read-Only Configuration Endpoints** - `GET /api/v1/shares/{name}/config` - Get share configuration - `GET /api/v1/network/{interface}/config` - Get network interface configuration - `GET /api/v1/settings/system` - Get system settings - `GET /api/v1/settings/docker` - Get Docker settings - `GET /api/v1/settings/vm` - Get VM Manager settings - New `ShareConfig`, `NetworkConfig`, `SystemSettings`, `DockerSettings`, `VMSettings` DTOs - New `ConfigCollector` with parsers for all config files **Phase 2.2: Configuration Write Endpoints** - `POST /api/v1/shares/{name}/config` - Update share configuration - `POST /api/v1/settings/system` - Update system settings - Automatic backup creation before config updates (.bak files) - JSON request body validation - Error handling for write operations #### Disk Settings Feature - `GET /api/v1/settings/disks` - Get global disk settings - New `DiskSettings` DTO with fields: - `spindown_delay_minutes` - Default spin down delay (critical for Home Assistant) - `start_array` - Auto start array on boot - `spinup_groups` - Enable spinup groups - `shutdown_timeout_seconds` - Shutdown timeout - `default_filesystem` - Default filesystem type - Reads from `/boot/config/disk.cfg` - Enables Home Assistant to avoid waking spun-down disks #### Documentation - Complete API reference guide (`docs/api/API_REFERENCE.md`) - API coverage analysis (`docs/api/API_COVERAGE_ANALYSIS.md`) - WebSocket events documentation (`docs/WEBSOCKET_EVENTS_DOCUMENTATION.md`) - WebSocket event structure guide (`docs/WEBSOCKET_EVENT_STRUCTURE.md`) - Phase 1 & 2 implementation report (`docs/implementation/PHASE_1_2_IMPLEMENTATION_REPORT.md`) - Disk settings implementation report (`docs/implementation/DISK_SETTINGS_IMPLEMENTATION.md`) - Deployment guides (`docs/deployment/`) - Documentation index (`docs/README.md`) #### Core Features - Comprehensive system monitoring (CPU, memory, uptime, temperature) - Array status monitoring (state, parity, disk counts) - Per-disk metrics (SMART data, temperature, space usage, spin state) - Network interface monitoring (bandwidth, IP addresses, MAC addresses) - Docker container monitoring and control - Virtual machine monitoring and control - UPS status monitoring - GPU metrics monitoring - User share monitoring - REST API with 46 endpoints - WebSocket support with 9 event types - Event-driven architecture with pubsub - Graceful shutdown and panic recovery - Automatic log rotation ### Changed - Updated deployment script to include `--port 8043` flag - Improved error handling in collectors - Enhanced logging with structured messages - Optimized data collection intervals ### Fixed - Fixed logger method calls (changed `logger.Warn` to `logger.Debug`) - Fixed API server port configuration - Fixed icon display in Unraid Plugins page - Fixed backup creation in deployment script ### Deployment - Deployed to live Unraid server (192.168.20.21) - All endpoints tested and verified - Service running on port 8043 - Icon fix verified in Unraid UI --- ## API Endpoint Summary ### Total Endpoints: 46 **Monitoring** (13): - GET /api/v1/health - GET /api/v1/system - GET /api/v1/array - GET /api/v1/disks - GET /api/v1/disks/{id} - GET /api/v1/shares - GET /api/v1/docker - GET /api/v1/docker/{id} - GET /api/v1/vm - GET /api/v1/vm/{id} - GET /api/v1/ups - GET /api/v1/gpu - GET /api/v1/network **Control** (19): - POST /api/v1/docker/{id}/start - POST /api/v1/docker/{id}/stop - POST /api/v1/docker/{id}/restart - POST /api/v1/docker/{id}/pause - POST /api/v1/docker/{id}/unpause - POST /api/v1/vm/{id}/start - POST /api/v1/vm/{id}/stop - POST /api/v1/vm/{id}/restart - POST /api/v1/vm/{id}/pause - POST /api/v1/vm/{id}/resume - POST /api/v1/vm/{id}/hibernate - POST /api/v1/vm/{id}/force-stop - POST /api/v1/array/start - POST /api/v1/array/stop - POST /api/v1/array/parity-check/start - POST /api/v1/array/parity-check/stop - POST /api/v1/array/parity-check/pause - POST /api/v1/array/parity-check/resume - GET /api/v1/array/parity-check/history **Configuration** (13): - GET /api/v1/shares/{name}/config - POST /api/v1/shares/{name}/config - GET /api/v1/network/{interface}/config - GET /api/v1/settings/system - POST /api/v1/settings/system - GET /api/v1/settings/docker - GET /api/v1/settings/vm - GET /api/v1/settings/disks **WebSocket** (1): - GET /api/v1/ws --- ## API Coverage | Category | Coverage | Status | | ------------------ | -------- | ---------- | | **Overall** | **60%** | 🟡 Partial | | Monitoring | 85% | ✅ Good | | Control Operations | 75% | ✅ Good | | Configuration | 40% | 🟡 Partial | | Administration | 0% | 🔴 None | --- ## WebSocket Events **Total Event Types**: 9 - `system` - System metrics updates - `array` - Array status changes - `disk` - Disk status changes - `docker` - Docker container events - `vm` - VM state changes - `ups` - UPS status updates - `gpu` - GPU metrics updates - `network` - Network statistics updates - `share` - Share information updates --- ## Known Issues None at this time. --- ## Planned Features ### Phase 3: Advanced Configuration - Network configuration write endpoint - Docker settings write endpoint - VM settings write endpoint - Per-disk spindown override settings ### Phase 4: User Management - User list endpoint - User permissions endpoint - User creation/modification endpoints ### Phase 5: Plugin Management - Plugin list endpoint - Plugin install/update/remove endpoints - Plugin settings endpoints ### Future Enhancements - Historical data storage - Alerting and notification system - Network statistics trending - Enhanced SMART attribute monitoring --- ## Migration Guide ### From Pre-1.0.0 Versions This is the initial release. No migration required. --- ## Contributors - Ruaan Deysel (@ruaan-deysel) --- ## Links - **GitHub Repository**: - **Documentation**: [docs/README.md](docs/README.md) - **API Reference**: [docs/api/API_REFERENCE.md](docs/api/API_REFERENCE.md) - **Issues**: --- **Last Updated**: 2025-11-03 **Current Version**: 2025.11.0