CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to Linux Doctor are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/) and versioning follows
[SemVer](https://semver.org/).
## [Unreleased]
## [0.8.4] - 2026-11-06
### Fixed
- **A freshly booted machine was told its backups were broken.** `systemctl`
resets `LastTriggerUSec` to 0 at boot for a timer that is not
`Persistent=true`, so the property reads "never ran" until the timer first
elapses — a daily timer produced a medium `backup/stale` from boot until
noon, then cleared itself when it ran. Zero is now only evidence when the
machine has been up longer than the stale window; an unreadable uptime stays
unknown, like an unreadable trigger value.
- **A browser's memory was reported at roughly twice its real size.** The
check summed RSS across every process of an app, which counts each shared
page once per process: 64 Brave processes measured 8.19 GB of RSS against
4.16 GB of unique memory (PSS), turning 27% of RAM into 53% and crossing the
40% threshold on its own. PSS is used whenever the kernel exposes it, with
RSS as the per-process fallback, so a shared library is counted once. Anyone
who saw a `processes/high` or `processes/warn` finding before will now see
the smaller, real number.
- **The support bundle leaked the hostname and account name.** It promised "no
hostnames, usernames" and redacted IP literals and `/home/<user>` paths, but
a hostname inside a unit name, or a username in a path outside `/home`, went
through untouched — nothing in the code knew what either one was. Both are
now redacted wherever they appear in the bundle, except when the hostname is
already part of the distro name (on stock Bazzite the hostname *is* the
distro name, and rewriting it would break the one field a maintainer needs).
- **zram swap was described as disk swapping.** "6.6 GB is being pushed to
swap" appeared next to a `zram/ok` finding saying the compressed swap was
healthy. Swap backed by zram is compressed RAM, not I/O pressure; the
sentence now says that, and swap on a real block device keeps the old
wording.
- **An unwritable state directory could hang the whole scan.** Node's
recursive `mkdir` never returns on procfs instead of failing, so
`XDG_CACHE_HOME` or `XDG_DATA_HOME` pointing into `/proc` froze the run
(>45 s, no progress). A write aimed at procfs or sysfs now fails
immediately, like any other unwritable directory.
## [0.8.3] - 2026-11-03
### Fixed
- **The app reported software rendering on a working GPU.** The AppImage shell
sets `LIBGL_ALWAYS_SOFTWARE=1` for the webview, and the subprocess that runs
the checks inherited it: `gpu` asked Mesa which renderer was active, got
llvmpipe, and reported "GPU acceleration is not active (software rendering)"
as a medium finding on a machine whose GPU was fine. The shell's environment no
longer reaches the checks — a diagnostic should observe the system, not its own
workaround.
- **Two routine journal lines were reported as unrecognized errors.** PipeWire
loses an ALSA device when the monitor sleeps (`iec958:0p: snd_pcm_drop: No such
device`) and dbus-broker cannot activate NetworkManager's dispatcher during
early boot; both reach priority err and both are routine. Thirty such lines on
a healthy desktop became an "unrecognized entries" finding.
- **The first run claimed "34 unchanged" with no history at all.** The diff
between runs returned "everything unchanged" when there was no previous run to
compare against, so a brand-new install greeted the user with a statement that
could not be true. It now says `SINCE LAST RUN first run — no baseline to
compare against` (and `# since: first run (no baseline)` under `--plain`), with
`diffSinceLast.firstRun` in the JSON. A second run still reports changes
normally.
### Changed
- **The baseline gate no longer judges the CI runner.** `disk/full` fired on the
runner's own `/__t` workspace at 83% and failed a pull request that had not
touched the disk check; `network/dns-slow` did the same with a latency
threshold. Both are host-dependent, like the other codes the gate already
tolerates.
- **`docs/RELEASING.md` shows the real signature check.** Step 10 listed
`node scripts/verify-updater-signature.mjs` with no arguments; the script takes
the public key, the `.sig` and the signed file, so following the checklist only
printed usage.
- Dependency updates: the cargo minor-patch group and `@tauri-apps/cli`.
## [0.8.2] - 2026-10-31
### Fixed
- **The AppImage came up as a blank window on a current Mesa.** The AppImage
bundles the build host's WebKitGTK, and WebKit's `PlatformDisplay` calls
`CRASH()` when it cannot create an EGL display: the Ubuntu builds (2.44 on
22.04, 2.52.6 on 24.04) both fail with `EGL_BAD_PARAMETER` on Mesa 26, so the
window stayed empty and every launch left a `WebKitWebProcess` core behind.
The AppImage is now built in a Fedora container, whose WebKit 2.54.1 renders
the same page on the same machine, same session and same GPU. The `.deb` and
`.rpm` stay on Ubuntu — built on Fedora they would declare
`libappindicator3-1`, the deprecated package, instead of the
`libayatana-appindicator3-1` that Debian and Ubuntu actually ship.
- **A high crash count no longer blames the hardware by default.** The coredumps
finding said a high crash rate "usually points at a broken library, a faulty
update, or a hardware issue (bad RAM)". When 26 of 33 cores turned out to be
one broken AppImage build aborting in WebKit, that sentence sent the reader to
`memtest86+`. The finding now groups crashes by executable and names the
program responsible; the hardware hypothesis is offered only when the crashes
really are spread across unrelated programs.
### Changed
- Dependency updates: the cargo minor-patch group (7 crates), `@tauri-apps/cli`,
and the pinned GitHub Actions.
## [0.8.1] - 2026-10-24
### Security
- **Two TLS trust advisories closed.** `rustls-webpki` 0.103.15 → 0.103.16 and
`webpki-root-certs` 1.0.9 → 1.0.10 (RUSTSEC-2026-0382 and RUSTSEC-2026-0383:
certificate verification could accept distrusted intermediates, and the
bundled root store trusted distrusted emSign intermediates). Both sit in the
HTTPS client the updater uses. The updater still verifies the minisign
signature before installing anything, so this was never a route to a malicious
update — but a trust store should not be wrong.
### Fixed
- **A failed service is no longer reported as a likely broken package.** The
`services/failed` finding said a failed unit "usually means a broken package, a
bad configuration, or a hook that errored". That is wrong for the common case:
a oneshot or timer-driven unit — a firmware refresh, a package cache refresh, a
backup job — often fails once on a transient problem and keeps the failed mark
until something clears it. The finding now says the state is a fact, sends you
to the unit's log first, and explains the difference.
- **The commands in that finding follow the unit's scope.** It offered
`systemctl --user status` in parentheses for every systemd system, including
when the failed unit was a system unit — a command that cannot show that unit.
A system unit now gets `systemctl status` / `journalctl -u … -b` /
`sudo systemctl reset-failed`; a user unit gets the `--user` forms, without
sudo.
- **"These 1 service are in a failed state."** The singular case now reads in the
singular.
- **The README said the tool's history lives under the config directory.** It
lives under `$XDG_DATA_HOME` — `~/.local/share/linux-doctor/history.json`. The
intro now says "the standard XDG directories (config, data, cache)", which is
true for all three; the exact paths stay in the sections that document them.
- **The README claimed the tool never changes anything, and documented
`--fix --yes` on the same page.** The read-only boundary is now stated as the
four separate things it is: diagnostics read and write nothing, the tool writes
its own data, the system changes only through `--fix --yes` running the
`[apply]` commands you opted into, and plugin code is your own and runs with
your privileges.
### Added
- **A worked case study: `docs/case-studies/inode-exhaustion.md`.** One finding
read end to end — "no space left on device" with 56 MB free — on a throwaway
64 MB ext4 image with 320 inodes. It shows the finding as the terminal prints
it and as JSON, the commands that confirm it without trusting the tool, why
inodes are the cause, and the fix with three independent confirmations. It is
a lab, and says so at the top and again in what it does not prove.
### Changed
- **CI is far less exposed to a slow Ubuntu mirror.** The Rust jobs bounded the
`apt` step, cache the downloaded packages, and stop re-fetching them on every
run. A slow runner used to look like a slow Rust build; now it names the step
that stalled. The first run on a cold cache still needs a working mirror.
## [0.8.0] - 2026-10-17
### Added
- **`--plugins`: see where each custom check comes from.** `--list` showed the
plugin's id but not its source, so answering "which file is this check?"
meant listing the directory by hand. The new flag prints
`id — /absolute/file.js — (user plugin)` for every active plugin, sorted by
id (or `none` when the directory is empty). Broken and id-colliding files
are reported on stderr and not listed, because they are not running.
- **The dashboard can manage persistent state now.** The ignore list was
write-only from the GUI (Dismiss), so a dismissed finding could not be seen
or undone once the toast was gone; the Checks modal lists every ignored
pattern and code with a Remove, and the config path. History gained a Clear
history button. Both are contextual, not a new Settings page, and the
endpoints are loopback-only like the rest of the dashboard.
- **Package-health check for Alpine.** `packages` had no `apk` branch, so
Alpine was the last family without one. `apk fix --simulate` is the dry run
(it reports what it would repair and changes nothing) and runs unprivileged.
- **Package-health checks for openSUSE and Void.** `packages` covered apt,
dnf and pacman only, so those two families got no package-manager check at
all. openSUSE uses `zypper verify -D` (the dry run, so it cannot fix
anything) and Void uses `xbps-pkgdb -a`; both read-only.
- **A bring-up check for devices that never appear.** `hardware` looks for
errors on devices that are working; this looks at the devices that are
missing. Three causes, one story: firmware the kernel could not load
(`bringup/firmware`), USB devices that failed to enumerate (`bringup/usb`),
and controllers with no driver bound (`bringup/driver`). This is the "my
Bluetooth and USB are gone" case: on a laptop the Bluetooth adapter is an
internal USB device, so one controller problem takes out both. Read-only,
current boot only, and it says so explicitly when everything is fine
(`bringup/ok`) instead of staying silent.
### Fixed
- **A dashboard refresh could get an empty response.** The stale-while-
revalidate background scan resolved to `undefined`; a blocking request that
arrived while it was in flight awaited it and served that `undefined` as an
empty body ("Unexpected end of JSON input"), and poisoned the cache with
it. The scan now always resolves to the body string.
- **A Re-run no longer waits on a depsolve that never finished.** `packages`
ran `dnf check` — a full depsolve of every installed package, ~15 s on a
2 900-package image — under the generic 8 s command cap, so on Fedora
systems it was killed and the check answered nothing at all. It now gets the
package-manager budget, caches a clean depsolve for ten minutes
(`LINUX_DOCTOR_PACKAGES_TTL_MS`; bad news is never cached), and on immutable
images (Bazzite, Silverblue, bootc) verifies the RPM database instead of
re-solving an image that was solved at build time — and says so in its
evidence. A full refresh on the reference machine went from ~8.6 s to
~2.2 s.
- **A Re-run could be swallowed by a background poll and record no
history.** With stale-while-revalidate, the background scan and the blocking
path shared one single-flight promise, so a Re-run that arrived while a
background poll was running awaited that non-saving scan and never advanced
history. An explicit Re-run now runs on its own, then refreshes the cache.
- **The dashboard refreshes live.** Auto-refresh polls every 5s instead of
20s, and the server answers a stale request instantly with the last report
while it regenerates in the background (stale-while-revalidate), so a poll
never waits on a full scan. The new report appears on the next poll.
- **`timers`, `fstrim`, `hardware` and `bringup` say when they cannot run.**
On a non-systemd system they returned silently, which read as "nothing
wrong". They emit an explicit skip now (`timers/skipped`, `fstrim/skipped`,
`hardware/skipped`, `bringup/skipped`), and `bringup` keeps running its
driver check even when the kernel log is not readable.
- **The dashboard can manage persistent state now.** The ignore list was
write-only from the GUI (Dismiss), so a dismissed finding could not be seen
or undone once the toast was gone; the Checks modal lists every ignored
pattern and code with a Remove, and the config path. History gained a Clear
history button. Both are contextual, not a new Settings page, and the
endpoints are loopback-only like the rest of the dashboard.
- **Package-health check for Alpine.** `packages` had no `apk` branch, so
Alpine was the last family without one. `apk fix --simulate` is the dry run
(it reports what it would repair and changes nothing) and runs unprivileged.
- **Package-health checks for openSUSE and Void.** `packages` covered apt,
dnf and pacman only, so those two families got no package-manager check at
all. openSUSE uses `zypper verify -D` (the dry run, so it cannot fix
anything) and Void uses `xbps-pkgdb -a`; both read-only.
- **A bring-up check for devices that never appear.** `hardware` looks for
errors on devices that are working; this looks at the devices that are
missing. Three causes, one story: firmware the kernel could not load
(`bringup/firmware`), USB devices that failed to enumerate (`bringup/usb`),
and controllers with no driver bound (`bringup/driver`). This is the "my
Bluetooth and USB are gone" case: on a laptop the Bluetooth adapter is an
internal USB device, so one controller problem takes out both. Read-only,
current boot only, and it says so explicitly when everything is fine
(`bringup/ok`) instead of staying silent.
- **The scheduled timer's service showed as failed on every machine that had a
finding.** `--notify` exits 1 when there are high/medium findings — that is
the documented contract (`docs/compatibility.md`) — but the generated unit had
no `SuccessExitStatus`, so systemd read a successful watchdog run as a failed
unit, and the `services` check then reported that failure. The unit declares
exit 1 as a success now. An existing install needs the same line plus
`systemctl --user daemon-reload && systemctl --user reset-failed linux-doctor.service`.
- **A container stopped with `podman stop` was called "did not exit cleanly".**
143 is 128 + SIGTERM, exactly what `podman stop`, `systemctl stop` and an
orderly shutdown send; 130 is SIGINT. Both are deliberate stops, not failures,
and this check is not a monitor — the medium finding cost 8 points on any
machine with a container switched off.
- **The "N checks · M skipped" chip could never be seen.** The renderer filled
it with the counts and cleared `hidden`, and the click handler that opens the
checks matrix was wired — but a stylesheet hid it with `display: none`, so
the element and its shortcut were dead. It shows next to the NEW/FIXED pills.
- **The history charts work from the keyboard.** Every dot and every stacked
bar opens that run's diff, but they were click-only: no `tabindex`, no role,
no accessible name and no focus ring — unreachable without a mouse, and
invisible to a screen reader. They are buttons now, with Enter/Space, a label
that names the run, and a focus ring.
- **A toast button was unreadable on the terminal theme.** The toast's action
button drew white text on the accent colour — 3.8:1 in dark, and 1.3:1 under
the terminal theme's bright green. It now uses the same dark-on-accent ink as
the primary buttons.
- **Dead and duplicated CSS went.** `.trend-hero`, `#autorefresh.on` and its
`.paused` state were each declared twice, the second silently winning; a
`.gauge.large` variant, `.status::after`, a print `.statusmsg`, a `#nexthep`
flex rule that could never apply and a redundant `.sb-item` rule had no
emitter at all. No pixels change — the survivors keep every property that was
in effect.
- **The phone toolbar wraps instead of stacking.** On a narrow screen every
control in the toolbar became a full-width block — six rows of chrome before
the first finding. The buttons now share rows of their own width; only the
filter chips and the search take a full row.
- **The desktop workbench starts at 1200px, not 1440px.** The wide layout — the
sidebar rail, the master-detail pane and the status bar — was gated at 1440
CSS px, which an ordinary 1366px laptop (or a 1080p screen at 150% scaling,
an effective 1280) never reaches; those machines got the phone-first column
instead. Two gates had to agree by hand (`wide.css` and `ui-wide.js`) with
nothing keeping them in sync, so a test now asserts they match and that the
committed `index.html` carries both.
### Changed
- **A new interface.** Edge-to-edge shell with a rail that carries the health
instrument, findings as rows instead of cards, one accent with exactly three
jobs (primary action, selected row, focus), small radii. `Ctrl+K` opens a
command palette over views, actions and the findings already in memory.
- Severity inks were re-derived (the info ink no longer equals the accent) and
every ink/surface pair is verified for contrast in all three themes.
### Fixed
- **The "recommended next step" was wrong on some systems.** Fedora Atomic
(Bazzite, Silverblue) was told to use `dnf` — the NVIDIA driver ships with the
image, there is no package cache to clear, the kernel is not a removable
package, and boot entries are not made by `grub2-mkconfig`. OpenRC (Alpine,
Gentoo) and runit (Void) users were told to run `systemctl`. openSUSE
Tumbleweed now gets `zypper dup`. The cache fix's `du` command expanded a glob
that could fail and skipped hidden entries.
- **Read-only is enforced, not just documented**: a test reads every command the
checks hand to the runner and fails on `sudo`, a mutating verb, a service
action, or a package manager in a non-query mode.
## [0.7.3] - 2026-10-10
### Fixed
- **The scheduled timer's service showed as failed on every machine that had a
finding.** `--notify` exits 1 when there are high/medium findings — that is
the documented contract (`docs/compatibility.md`) — but the generated unit had
no `SuccessExitStatus`, so systemd read a successful watchdog run as a failed
unit, and the `services` check then reported that failure. The unit declares
exit 1 as a success now. An existing install needs the same line plus
`systemctl --user daemon-reload && systemctl --user reset-failed linux-doctor.service`.
- **A container stopped with `podman stop` was called "did not exit cleanly".**
143 is 128 + SIGTERM, exactly what `podman stop`, `systemctl stop` and an
orderly shutdown send; 130 is SIGINT. Both are deliberate stops, not failures,
and this check is not a monitor — the medium finding cost 8 points on any
machine with a container switched off.
- **The "N checks · M skipped" chip could never be seen.** The renderer filled
it with the counts and cleared `hidden`, and the click handler that opens the
checks matrix was wired — but a stylesheet hid it with `display: none`, so
the element and its shortcut were dead. It shows next to the NEW/FIXED pills.
- **The history charts work from the keyboard.** Every dot and every stacked
bar opens that run's diff, but they were click-only: no `tabindex`, no role,
no accessible name and no focus ring — unreachable without a mouse, and
invisible to a screen reader. They are buttons now, with Enter/Space, a label
that names the run, and a focus ring.
- **A toast button was unreadable on the terminal theme.** The toast's action
button drew white text on the accent colour — 3.8:1 in dark, and 1.3:1 under
the terminal theme's bright green. It now uses the same dark-on-accent ink as
the primary buttons.
- **Dead and duplicated CSS went.** `.trend-hero`, `#autorefresh.on` and its
`.paused` state were each declared twice, the second silently winning; a
`.gauge.large` variant, `.status::after`, a print `.statusmsg`, a `#nexthep`
flex rule that could never apply and a redundant `.sb-item` rule had no
emitter at all. No pixels change — the survivors keep every property that was
in effect.
- **The phone toolbar wraps instead of stacking.** On a narrow screen every
control in the toolbar became a full-width block — six rows of chrome before
the first finding. The buttons now share rows of their own width; only the
filter chips and the search take a full row.
- **The desktop workbench starts at 1200px, not 1440px.** The wide layout — the
sidebar rail, the master-detail pane and the status bar — was gated at 1440
CSS px, which an ordinary 1366px laptop (or a 1080p screen at 150% scaling,
an effective 1280) never reaches; those machines got the phone-first column
instead. Two gates had to agree by hand (`wide.css` and `ui-wide.js`) with
nothing keeping them in sync, so a test now asserts they match and that the
committed `index.html` carries both.
## [0.7.2] - 2026-10-03
### Changed
- **The desktop app's binary is `linux-doctor-app` now.** The CLI and the
desktop build both shipped a binary called `linux-doctor`, so the GUI binary
was renamed to keep them apart. The `.desktop` entry and its window class
follow it — an unchanged `StartupWMClass` left the app with a duplicate dock
icon.
- **The shipped `.desktop` files validate clean.** They declared `Diagnostics`,
which is not a freedesktop category — an outright `desktop-file-validate`
error in the npm `packaging/` files — and then listed two main categories
(`System;Utility;`), which can show the app twice in the menu. The pair is
`System;Monitor;` now, with a `GenericName` added.
### Fixed
- **`orphans` no longer calls a failed query a tidy database on apt, zypper
or pacman.** Only the dnf branch checked whether its query actually ran; the
other three piped through `grep`/`grep -c`, so a locked apt or a failed
zypper printed nothing and the check reported "No orphaned packages". They
read the raw output and count in JS now, and stay silent when the query did
not run.
- **`backup` no longer says "nothing is scheduled" when it cannot read the
schedule.** On a non-systemd system with a backup tool installed the timer
list was empty for lack of `systemctl`, not for lack of a schedule; it says
the schedule could not be checked now (`backup/unknown`).
- **`reboot` no longer says "No reboot needed" in a container.** A
container has no kernels of its own, so the empty `/boot` list read as "the
running kernel is the newest". It skips in a container, and stays silent
when no kernel list can be read.
- **`boot`'s `df` probe no longer runs through `tail`.** Its exit status
belonged to `tail`, so a failed `df` looked successful; the data row is
taken in JS now.
- **`processes` sums memory per app, not per process.** A browser spreads
its memory across many processes that share one binary, so the check listed
"brave" twice, reported one process as if it were the app's footprint, and
missed the real total. The top consumers are summed per binary now.
- **`processes` no longer flags a large app when there is plenty of free
RAM.** It warned whenever one app used more than 20% of total RAM, ignoring
how much was actually available, so a 15GB machine with 9.4GB free got a
medium "a single app is using a lot of memory" for a browser. The warning
now requires the system to be short on memory (available/total below the
memory warn ratio); otherwise the top consumers stay informational.
## [0.7.1] - 2026-09-26
### Fixed
- **`memory`, `zram` and `fstrim` no longer report the HOST's state inside a
container.** `/proc/meminfo`, `/proc/swaps` and `lsblk` are not namespaced,
so a 256MB-limited container's `free -b` reported the host's 15GB — the same
lie `load` used to tell. The container detection is now shared
(`detectContainer`) and all four checks skip with an explanation.
- **`processes` works on Alpine now.** The check asked `ps` for
`--sort=-rss`, which busybox does not support, and busybox also ignores the
column after `args=`, so the rss number was lost and the empty result read
as "no consumers". It uses `ps -eo rss,args` (the one column order that
works on both procps and busybox) and sorts in JS.
- **`timers`, `fstrim` and `hardware` say when they cannot run.** On a
non-systemd system, or when lsblk/journalctl is missing, they returned
silently, which read as "nothing wrong". They emit an explicit skip now.
- **Checks that could not run now say so instead of staying silent.** The
`missing` flag was only set when the shell itself was absent, which never
happens: a missing tool exits 127 through the shell instead. So every check
that gated a "could not check" skip on `missing` stayed silent, and a
minimal system scored as if those checks had passed. `run()` marks 127 as
missing now, which activates the skip findings that were already written
(`memory/skipped`, `journald/skipped`, `smart/skipped`, and more), and
`processes`, `ports` and `certs` gained the explicit skips they lacked.
A score now comes with the list of what could not be checked: on a minimal
Debian image that is memory, processes, ports and the systemd checks, each
naming the package that would fix it.
- **Three update checks were answering a question no one asked.** Found by
running the engine against real package managers, not by reading code.
`zypper` counted through `awk`, which is not installed on a minimal openSUSE
image: the pipeline produced nothing and Tumbleweed reported "System is up
to date" with thirteen lines of updates on screen. `apk info -u` is not a
valid command (it exits 1 with "unrecognized option 'u'"), so Alpine
silently reported nothing at all. Void had no update branch, so a machine
with 54 pending updates was skipped and scored as current. All three read
the raw output and parse it in JS now, with no `awk`/`wc` dependency.
- **apt with an empty package index no longer claims "up to date".** A fresh
image, or a machine that never ran `apt update`, answers "0 upgraded" with a
zero exit status. That is not being up to date, and reporting it as such is
the dangerous direction of being wrong: the check says it could not
determine the state and points at `apt update`.
- **The KDE lock screen's retry message is no longer counted as a system error.**
`kscreenlocker_greet` logs `Authentication attempt too soon` when you retype a
wrong password quickly, and repeats it a few times, so a healthy desktop got a
medium "6 recognized errors" worth 8 points of health score. Only the screen
locker's copy is noise: the same string from `sshd` stays an error, because
that one is worth seeing. Also fixes `scripts/screenshot.mjs`, which waited
for a selector that does not exist and therefore never ran.
- **Flatpak app caches are measured now.** The cache check looked at `~/.cache`
and Trash only, so the per-app caches under `~/.var/app/<id>/cache` were
invisible, which on a Flatpak-heavy distro is the larger half: on the
maintainer's Bazzite box `~/.cache` held 3.5 GB while `~/.var/app` held
5.3 GB, and the report said nothing about it. Both are measured and counted
toward the same thresholds now, and the biggest offenders are named.
- **`zram` no longer calls a half-full compressed swap "healthy".** High
occupancy is not a fault by itself (zram is meant to be used and cold pages
sit there for days), so the severity stays informational. What changed is
the wording: it reports how much is compressed, says the kernel is working
to keep up, and points at the `processes` and `cache` findings. Inflating it
to medium would be grading by distance from normal.
- **`network` called a missing `iproute2` "no default route".** Minimal
Debian, Fedora and Ubuntu images do not ship `ip`, and the probe's failure
was read as an empty route table: a medium "No default network route" on a
machine with a perfectly good route. The tool's presence is probed
separately now, and a missing `ip` is an explicit skip with the install
hint.
- **`load` is skipped inside a container.** `/proc/loadavg` is not namespaced:
in a container it is the host's load average while `nproc` reports the
container's CPUs, so the ratio invented an overloaded system out of an idle
container (all five test images reported it). The check says why it is
skipping and stays quiet there.
- **`hardware` answered "no errors" only by accident.** The check used the
exit status of `journalctl ... | grep ...` as its readability gate, but grep
exits 1 when nothing matches, so its status meant "found something", not "I
could read the log". On a healthy machine with no MCE/EDAC line at all it
said nothing instead of "No hardware errors logged". A benign EDAC banner
made grep exit 0 on the maintainer's box, which hid it.
- **`timers` no longer calls a timer with an unmet start condition broken.**
`dnf-makecache.timer` is enabled on an immutable system and can never run:
its start condition is unmet by design (`systemctl status` says so). The
check asked only whether the timer was enabled and had never fired, so it
reported a broken schedule the user cannot fix. It consults
`ConditionResult` now.
## [0.7.0] - 2026-09-22
### Added
- **Two public documents about trust rather than features.**
[docs/limitations.md](docs/limitations.md) lists what the tool does not
detect and every false positive it has shipped and fixed, with the regression
test that guards each one;
[docs/compatibility.md](docs/compatibility.md) defines what counts as a
public API (flags, exit codes, the JSON schema, finding codes, config keys,
the check shape) and how a change to it is announced.
- **`F11` toggles fullscreen in the desktop app.** Three things had to agree for
that to work (the key handler in the dashboard, `withGlobalTauri` so the page
can reach the window API, and the `set_fullscreen` capability), so a test now
fails if any of them goes missing. The browser dashboard keeps using the
browser's own fullscreen.
### Fixed
- **The in-app update dialog now says what changed.** The updater manifest
carried an empty `notes` because the generator only looked at
`RELEASE_NOTES`, which the release workflow never sets: the GitHub release got
the changelog section and the updater got nothing. The generator reads the
section for the version out of `CHANGELOG.md` now, and an explicit
`RELEASE_NOTES` still wins.
- **The packaged AppStream metadata no longer lags a release behind.** The
`<releases>` list in the metainfo that app stores and `appstreamcli` read was
updated by hand, and it drifted: after 0.6.1 shipped the file still advertised
0.6.0. The release script adds the entry now (newest first, idempotent), the
0.6.1 entry is in place, and tests hold the rule.
- **`security/autologin` never read the Debian GDM config.** The probe listed
`/etc/gdm` (the Red Hat, openSUSE and Fedora layout) but not `/etc/gdm3`,
which Debian, Ubuntu and Mint use, so an enabled autologin there was read as
nothing at all. Both layouts are searched now, and a test pins the Debian
path. Adding it is only safe because the comment filter below stops the
commented examples that Debian's `daemon.conf` ships by default.
- **Five more wrong results, found by auditing every check against real input.**
Each one was reproduced before it was changed, and each has a regression test
that fails without the fix:
- **`security/autologin` fired on a commented example** (the
`# AutomaticLoginEnable=true` line several distros ship by default), on an
explicit `false`, and on a bare `[Autologin]` section header. Comments are
ignored now, GDM needs a true value, and SDDM needs a `User=` line inside
the section.
- **`flatpak` reported "apps are up to date" with updates pending.** The
default `remote-ls --updates` output is a column table whose fields contain
no `/`, and that is what the old count looked for. It asks for
`--columns=application,version` now, with a table fallback for older
flatpak.
- **A `FAULTED`, `UNAVAIL`, `REMOVED` or `SUSPENDED` ZFS pool was reported as
"RAID arrays are healthy".** Only the word "degraded" was recognised, so
everything worse fell through to the healthy branch. Only `ONLINE` counts as
healthy now, and a running scrub is no longer called a rebuild.
- **A full root filesystem was reported as a full `/boot`.** `df -P /boot`
prints the root row when `/boot` is a directory on `/`, and the columns look
identical. The row is only used when its mount point is the requested one.
- **`crash` could report a kernel panic from the Intel machine-check boot
banner** (`Intel machine check reporting enabled on CPU#N`) on machines that
reboot often. It defers to the same classifier the hardware check uses.
- **`packages/locked` accused linux-doctor of holding the dpkg lock.** The lock
probe ran beside the tool's own `apt-get check`, and the `updates` and
`orphans` checks take the same frontend lock, so `fuser` found linux-doctor
itself. Run as root on a healthy machine that produced a medium finding
naming the tool's own process (#24). Only holders outside linux-doctor's
process group are reported now, and the evidence lists the PIDs alone.
- **`packages/locked` reports nothing when it cannot read its own process
group.** Without `ps` to tell linux-doctor's own package-manager processes
apart from another process, every lock holder would look foreign — exactly the
false positive #24 removed. The attribution moved from the shell pipeline into
JS, where a test pins the quiet fallback (#25).
## [0.6.1] - 2026-09-17
### Fixed
- **`hardware/ecc` fired on machines with no ECC.** The check matched the EDAC
driver's own init lines (`EDAC MC: Ver: ...` and `EDAC ie31200: No ECC
support`), so a healthy box reported a corrected memory error; on some systems
that was the top item in the report. It needs a real CE/UE event now.
- **`hardware/mce` fired on the MCE banks boot line.** `mce: CPU supports N MCE
banks` is printed once per CPU at boot on every Intel machine, and it was
reported as a machine check exception (high). Routine init lines are rejected.
- **`fs/btrfs-errors` fired on the btrfs module load banner.** `Btrfs loaded,
zoned=yes, fsverity=yes` is printed at boot on any kernel with btrfs compiled
in, mounted or not. The pattern requires a real event and still catches
`BTRFS critical`, `failed` and `corrupt`.
- **An uncorrected memory error was filed as "corrected".** An EDAC `UE` line
raised `hardware/ecc` at medium; it is high now, with its own wording.
- **`packages/broken` fired on apt's own lock refusal.** Without root,
`apt-get check` cannot take the dpkg frontend lock, and that refusal was read
as broken dependencies (high). The healthy finding says "skipped (needs root)"
instead of claiming the check ran.
- **`packages/broken` fired on `pacman -Dk`'s success line.** "No database
errors have been found!" contains the word "errors", so a clean Arch system
was reported as a broken package database.
- **`updates` doubled the count on dnf.** `dnf check-update` prints a header,
indented obsoletion lines and one entry per enabled repo, so 314 pending
updates read as 630. Unique package names are counted now.
- **`journal` counted stack-trace lines as separate entries.** One multi-line
entry was counted once per continuation line, which turned a few crashes into
"3062 unrecognized log entries".
- **The desktop webview could come up blank** where WebKit's DMA-BUF renderer
fails; it is disabled by default now.
### Changed
- **Install instructions lead with `npx linux-doctor`**, the published package,
with the GitHub form kept for the current `main`. The Arch section no longer
claims an AUR package that does not exist yet.
- **CI gates the checks against clean images and recorded machines.** The engine
runs inside Fedora, Debian, Alpine and Arch containers, and replays fixtures
recorded on real machines; both fail on an unjustified high or medium finding.
## [0.6.0] - 2026-09-13
> **Highlights:** the desktop grows into a real app — bundled Node runtime, tray + single-instance + autostart, **auto-update**, adaptive window — and the dashboard gets a wide-screen workbench (master-detail, status bar, deep-linkable state). A correctness pass fixes checks that were silently wrong or could never fire, and a hardening pass covers state files, outbound data and the release pipeline. No breaking changes; the JSON schema stays v1.
### Added
- **Desktop app: auto-update.** The app checks GitHub Releases for a newer
signed build ~12s after startup and from a tray **Check for updates** item;
a native dialog offers to install and restart. Artifacts are
minisign-signed (public key in `tauri.conf.json`, private key a CI secret),
the updater manifest is assembled by `scripts/make-latest-json.mjs`, and a
release fails fast if the signing secret is missing. On Linux the updatable
artifact is the AppImage (deb/rpm stay package-manager updates).
`LINUX_DOCTOR_NO_UPDATE=1` disables the check.
- **The desktop app is a tray app.** A second launch surfaces the existing
window (single-instance, no second report server losing the fixed port); the
tray offers Open / Run checks now / Start at login (official autostart
plugin) / Check for updates / Quit, all handled Rust-side (Tauri IPC is not
used in this stack). Missing-tray systems fail soft.
- **The desktop app bundles its own Node runtime.** .deb/.AppImage/.rpm ship a
pinned Node 22 LTS binary under `<resources>/runtime/node`, hash-checked
against the official SHASUMS256.txt at build time — end users need nothing
on their PATH. The fetch is architecture-aware (x64 + arm64), so a local
arm64 build embeds the right runtime. `LINUX_DOCTOR_NODE` still overrides.
- **Wide-screen desktop workbench (≥1440px).** The app shell uncaps and fills
the monitor; Overview becomes a master-detail view — the findings list stays
put while the selected finding renders in a pinned detail pane. Findings
groups open, rows densify, and a persistent status bar shows the active view,
freshness and the keyboard map. Below 1440px everything is unchanged.
- **Deep-linkable dashboard state.** View, severity filter, grouping, theme and
density travel in the URL (`?view=checks&sev=high&group=category&theme=terminal`),
so a refresh or a pasted link restores the exact workbench. Free-text search
is deliberately never written to the URL.
- **`--debug` command tracing and a per-check deadline.** `--debug` (or
`LINUX_DOCTOR_DEBUG=1`) traces every spawned command, its duration, status
and (on failure) the stdout/stderr tail to stderr — stdout stays
machine-clean. Each check is now capped at 45s wall-clock (recorded in
`checkErrors`), so a check with many sequential commands cannot stretch a run.
- **`--allow-private-endpoint`** for `--push`/`--alert`/`--heartbeat`/`--ai`:
opt in to a self-hosted LAN server that the new destination guard would
otherwise refuse.
### Changed
- **Dashboard "Re-run checks" records the run in history.** The trend and the
new/fixed diff advance when the user asks for a run (with a confirmation
toast); the 20s background auto-refresh still never writes history.
- **Redundant probes removed.** The Fedora package check no longer runs two
full `rpm -Va` verifications whose result was never used; the apt orphan
check runs `apt-get -s autoremove` once (count + sample); `glxinfo` is
memoized per run (was spawned by both the gpu and wayland checks); the
hardware check reads the kernel log once instead of twice.
- **The desktop window fits the screen** instead of a fixed 1500×950: centered,
clamped to the monitor minus a margin, never below 900×640, shown only after
sizing (no resize flash).
- **`npm run gui:build` / `gui:dev` fetch the bundled Node runtime first** (a
no-op when the right version is present), so a local build cannot package a
stale or missing interpreter.
- **Releases smoke-check the packages** before publishing: the gui job asserts
the built `.deb`/`.rpm` contain `runtime/node` and runs the bundled
interpreter.
- **Docs:** support-bundle privacy claims corrected; the plugin/Pro trust model
is stated in the README and configuration.md; README notes the bundled
runtime and its ~130 MB size.
### Fixed
- **`smart/failing` could never fire.** `smartctl -H` exits non-zero when a
drive is FAILING, and the code skipped any non-zero result before reading the
health string — a failing disk was silently ignored. The health string is now
read first; a dying disk is reported as high.
- **Desktop machines were classified as servers.** The profile probe read
`loginctl`'s UID column instead of the seat, so every battery-less desktop was
"headless" and all desktop checks (wifi, gpu, bluetooth, wayland, audio,
cache) were skipped. The probe now finds the seat token regardless of column
layout (shared with the wayland check).
- **The crash check reported the lifetime boot count.** `journalctl --list-boots`
ignores `--since`; the boot count is now computed from the JSON form and
filtered to the last 7 days — "62 reboots in the last 7 days" became the true
count.
- **Atomic detection on composefs-overlay distros.** `findmnt -T /` reports
`overlay` on Bazzite/Silverblue, so `immutable` disagreed with `imageBased`
and atomic skips plus the report note were suppressed; `immutable` now also
derives from the os-release `imageBased`/`bootc` signals.
- **The `locales` and `fds` checks could never fire.** `locales` runs with
`LC_ALL` unset for its one probe (the forced `LC_ALL=C` masked the exact error
it looks for); `fds` now measures per-process pressure against
`RLIMIT_NOFILE` (the real "too many open files" mode) because `fs.file-max` is
effectively unlimited on modern kernels.
- **One OOM kill looked like a pattern.** The oom check counted log lines; one
kill writes both a "Killed process" and a "reaped process" line, so it was
reported as two kills at high. It now counts distinct PIDs.
- **No more false "no firewall" without root.** `nft list ruleset` needs
CAP_NET_ADMIN; when the ruleset cannot be read and no firewall service is
active the result is a new `security/firewall-unknown` (info, no fix) instead
of claiming there is no firewall, and `ports` says "firewall status unknown".
- **Fedora-family updates had no safe fix.** The catalog switched on a
`family === "rhel"` case that `detectDistro` never returns (it normalizes to
`fedora`); Fedora/RHEL now get `sudo dnf upgrade`.
- **Alpine/BusyBox no longer go quiet.** Missing `getent` is `network/skipped`
(was a false "DNS is failing"); an unusable `df`/`df -i` is
`disk/skipped`/`inodes/skipped`; `free` without an `available` column falls
back to `/proc/meminfo`; `fstrim` no longer counts zram/loop devices as SSDs.
- **`durations` matches its schema.** `--json --profile` emitted an array while
the schema (and the dashboard) used a check→ms object; both channels now emit
the object.
- **A malformed threshold can no longer become 0.** `Number("")`/`Number([])`
are 0; only a real number or non-empty numeric string is accepted now (CLI,
dashboard and config share one coercion).
- **Dashboard Re-run/history, URL state, and the wide layout:** `?view=` deep
links win over the remembered view; search text is no longer written to the
URL; the desktop shell detects the webview via `tauri://localhost` too and
surfaces the real service error; the static `--html` export keeps the Skipped
section; sticky chrome is opaque (the toolbar/status bar no longer show
content through them) and the detail pane sticks below the toolbar.
- **Desktop app: the dashboard could never parse a report.** The loopback
report server wrote an extra CRLF after the CORS block, leaking
`Content-Length`/`Connection` into the JSON body. Fixed with a regression
test.
- **Start-at-login no longer lies.** The toggle re-reads the real autostart
state, reflects it in the checkbox, and logs the actual outcome; it is also
panic-safe.
- **Rust children drop `NODE_OPTIONS`/`NODE_PATH`** alongside `LD_*`, so a
poisoned environment cannot inject a preload module into the Node checks.
### Security
- **State files are written atomically and privately.** config.json (which may
hold the Pro license key), history, cache, the support bundle and systemd
units go through one `atomicWrite()` helper: a unique temp sibling opened
`O_CREAT|O_EXCL` (a planted symlink fails instead of being written through),
`0600` files in `0700` directories, replaced by rename.
- **Egress is checked by destination, not just scheme.** `--push`, `--alert`,
`--heartbeat` and `--ai` refuse private/LAN address literals (RFC1918,
link-local including the cloud metadata address, CGNAT, IPv6
ULA/link-local) unless `--allow-private-endpoint` is passed, and all four
refuse HTTP redirects. `LLM_BASE_URL` goes through the same guard: an API key
can no longer be sent to a plaintext non-loopback endpoint.
- **Destructive safe-fixes are now `[manual]`.** Enabling a firewall (ufw can
lock out SSH), package autoremove, container prune and Trash deletion are
printed but never auto-executed by `--fix --yes` — a false positive can no
longer delete packages/data or cut a remote session.
- **Outbound and shared data is scrubbed.** `--alert`/`--push` carry scrubbed
finding text; `--html` is scrubbed like `--md` (whole payload + hostname);
the support bundle walks every field (plugin extras included); endpoint
credentials are redacted from messages; `scrub()` is linear (a crafted colon
run could hang a run); `/run/media` and `/media` user paths are redacted.
- **`--html` can no longer be weaponized.** The payload is embedded through
`jsonForInlineScript()` (`<` → `\u003c`), so a `</script>` in any field
cannot close the tag; the dashboard header escapes its payload fields.
- **Loopback report endpoints are cached and single-flight.** Repeated/drive-by
requests share one scan, the tray's "Run checks now" coalesces, and
connections get a read timeout; dashboard polls never write history.
- **A corrupt config is no longer silent**, and the dashboard rejects
cross-origin writes and non-loopback Host headers as before.
- **Release pipeline hardened.** Every GitHub Action is pinned to a full commit
SHA; the default token is `contents: read` with write scopes only on the two
build jobs; publish/attest steps are gated on `refs/tags/v`; and the bundled
Node is hash-pinned (v22.23.2).
## [0.5.0] - 2026-09-05
> **Highlights:** 8 new server checks (TLS certs, exposed ports, fd pressure, RAID, containers, service restart loops, GPU memory, stale backups) — the catalog grows to **49 checks / 161 codes**; `--init` guided first-run setup; `--heartbeat <url>` dead-man's switch [Pro]; `--ai-local` private offline AI summaries; the dashboard becomes a five-view app (Overview / History / Checks / System / Schedule) with a Terminal theme and a machine wiki. No breaking changes — the JSON schema stays v1.
### Added
- **Terminal theme for the dashboard.** A phosphor-console look (near-black green-tinted background, monospace throughout, sharp corners, flat surfaces, green accents) as a fourth theme option behind the header button — same layout, same contrast budget, variables only.
- **Cockpit-style app navigation.** The five views move into a sidebar rail with icons and live count badges on wide screens (scrollable tab bar below that width); secondary panels share a two-column card grid beside START HERE instead of stacking full-width. Same renderers, same keyboard flows, same remembered choice.
- **Product-look pass on findings and hero.** Informational findings now render as compact one-line rows (dot + title + code, full body on open) instead of full cards; evidence summaries carry their line count; the hero's empty right side is now four stat tiles (problems, checks, clean streak, last check with the live checked-ago indicator).
- **Dashboard content order.** START HERE takes the full width first; the daily-check strip and the security posture share a two-column notices row beneath it (spanning full width when only one is visible) — the Overview spine reads hero → START HERE → findings with no information or action removed.
- **System view is a full machine wiki.** The report payload now carries architecture, hostname, CPU model, total RAM, desktop, and session type (additive schema fields, still v1); the view renders them as Operating system / Hardware / Session sections next to the report summary.
- **Dashboard app views: Overview / History / Checks / System / Schedule.** The single scroll now has homes: today's report stays exactly as-is under Overview (the default), History gains a newest-first run ledger beside the trend charts, the all-checks matrix renders inline under Checks, System shows machine facts plus the report summary, and Schedule shows timer status, cadence, notification state, and copy-paste setup/alert/heartbeat commands. Tabs carry live count badges; tab bar, `1`–`5` shortcuts, roving arrow-key tabs, remembered choice — existing filters, keyboard flows, and the auto-refresh pause behavior are untouched.
- **`--init` — guided first-run setup (Free).** Detects the environment (profile, systemd, node), then offers the three steps that turn a one-off run into set-and-forget monitoring: a starter config, the daily systemd timer, and a desktop-notification test. Every mutating step asks first; without a TTY it prints the same steps as copy-paste commands.
- **`--heartbeat <url>` [Pro] — dead-man's switch.** Pings a heartbeat URL (Healthchecks.io, BetterStack) with a bare GET after every completed run, in one-off and `--daemon` mode. The ping carries no body — liveness only — and failures are warnings, never exit-code changes. Same Pro gate and URL validation as `--alert`.
- **ntfy examples for `--alert` in docs/integrations.md.** Phone push via ntfy (self-hostable, no account) plus the `--heartbeat` complement, so scheduled runs reach a human without a fleet server.
- **4 new server checks — the catalog grows to 49 checks / 161 codes:**
- `certs` — TLS certificate expiry, the classic silent outage: reads certbot state (`/etc/letsencrypt/live/*/cert.pem`) *and* the actually-deployed cert via `s_client` against locally-listening TLS ports (catches "renewed on disk but never reloaded"). Expired or <7 days → **high**, <30 days → **medium**. Tunable `certWarnDays` / `certCritDays`.
- `ports` — risky services (MySQL, PostgreSQL, Redis, MongoDB, FTP/Telnet…) listening on non-loopback interfaces, cross-checked against the firewall (same signals as the security check). Exposed with no firewall → **medium**; firewalled or clean → informational.
- `fds` — file-descriptor pressure from `/proc/sys/fs/file-nr`: the "server dies mysteriously with no metric spiking" cause. ≥95% of the kernel limit → **high**, ≥90% → **medium**, healthy stays silent.
- `backup/stale` — extends the backup check: a scheduled backup timer that never triggered, or last ran over `backupStaleDays` (30) ago, is **medium**. A backup that never runs protects nothing.
- **Dashboard "Daily check" strip.** The report now opens with the scheduling
state: whether the user timer (`--install-timer`) is installed and active,
plus the browser-alerts state — with a one-click copy of the setup command
when the timer is off. Served read-only from the new `GET /api/schedule`
endpoint (no report-schema change); hidden for static `--html` exports.
- **New `--ai-local` flag — private, offline AI summaries.** Points the existing AI summary at a local Ollama instance (`http://localhost:11434/v1`, model `llama3.2`, any key) so the plain-English explanation runs entirely on your machine with no cloud and no `LLM_API_KEY` to a third party. Finding text is still redacted with the same `scrub()` before it ever leaves the box.
- **New `gpu-usage` check:** GPU memory pressure for AI/homelab rigs — reads VRAM used/total for NVIDIA (`nvidia-smi`) and AMD (`amdgpu` sysfs, no tool needed). Stays silent on an idle card, reports usage as informational when the GPU is working (≥50%), and flags VRAM nearly full (≥90%) as **medium** — the OOM risk when you try to load a bigger model. Complements the existing `gpu` driver-health check.
- **3 new server checks:**
- `raid` — software RAID health: a degraded mdadm array (parsed from `/proc/mdstat`, no binary needed) or a `DEGRADED` ZFS pool is flagged **high** (data-loss risk), a resyncing/scrubbing array is **medium**, healthy arrays get an informational line. Server-scoped (pass `--check=raid` to force it anywhere).
- `containers/dead` + `containers/oom` + `containers/restarting` — extend the container check: a container that exited non-zero is **medium**, one killed by the OOM killer (exit 137) is **high**, and one stuck in a restart loop is **medium**. Read-only via `podman ps -a` / `docker ps -a`.
- `services/restart-loop` — extend the services check: a unit in `auto-restart` substate is "active" but never actually stays up, so it hides behind a green status; flagged **high** with its `NRestarts` count.
### Fixed
- **AppImage runtime mounts no longer report as full disks.** FUSE mounts at `/tmp/.mount_*` (device `*.AppImage`) always read 100% because they are fixed-size images, not filling disks — same false-positive class as the excluded squashfs layers. Skipped by mount shape in the disk *and* inode checks.
## [0.4.0] - 2026-08-28
### Added
- **`--md <path>` — share-ready Markdown export.** One flag writes a
paste-ready Markdown report (START HERE, severity sections with stable
codes, evidence, fixes, since-last-run diff) with the same `scrub()` used
by support bundles applied to every text field — IPs, home paths, and UIDs
are redacted before the file is written, so it is safe to post in public
forums or issues. Exit codes match the normal report.
- **`--install-timer` / `--uninstall-timer` — one-command scheduling.**
Writes user-level systemd units (no sudo) under `~/.config/systemd/user/`
with resolved absolute paths (survives npm relocation), enables the timer,
and attaches `--notify` — the machine only speaks when something NEW
appears. Idempotent install, forgiving uninstall, honest exit 2 when
systemd is not running. Management commands never run checks.
### Changed
- **`pkgInstall()` no longer guesses `dnf` on unknown distributions.** Void
and Gentoo now get correct native commands (`xbps-install -Sy`, `emerge`);
any other unrecognized distro gets an honest manual-step line instead of a
wrong `sudo dnf install` (e.g. on NixOS).
## [0.3.5] - 2026-08-27
> **Highlights:** 6 new health checks, stronger fleet/AI privacy, safer `--fix`, and a more robust dashboard. No breaking changes.
### Security
- **Fleet reporting now blocks plaintext HTTP for authenticated pushes.** When `FLEET_API_KEY` is set, `--push` and `--alert` require `https://` (loopback `http://127.0.0.1`, `localhost`, `[::1]` exempt for local dev). The check runs at CLI validation and again before `pushReport`/`sendAlert`; a misconfigured `http://` endpoint now fails fast with exit 2 instead of leaking the Bearer token. Includes tests in `fleet.test.js` and `alert.test.js`.
- **AI summaries redact sensitive data before egress.** Finding titles and details are now scrubbed with the same `scrub()` used for support bundles — IPv4/IPv6 literals and `/home/<user>` paths never reach the LLM endpoint. Verified by `llm.test.js`.
### Added
- **6 new checks + 1 Flatpak extension (44 checks / 145 codes total):**
- `inodes` — inode exhaustion (`df -i`): the classic "No space left on device" when `df -h` still shows free space. Tunable `inodeFullPct` / `inodeWarnPct` (90/80).
- `orphans` — orphaned packages: `pacman -Qtdq`, `apt autoremove --dry-run`, `dnf repoquery --unneeded` / `zypper packages --unneeded`.
- `boot` — boot partition health: space on `/boot` and `/boot/efi` plus missing `grub.cfg` / `systemd-boot` entry.
- `cache` — user cache and trash bloat: `~/.cache` and `~/.local/share/Trash` (5/10 GB thresholds, desktop/laptop).
- `wifi` — WiFi state: rfkill soft/hard block, `nmcli radio wifi`, adapter presence (`wifi/blocked`, `wifi/disabled`, `wifi/no-adapter`, `wifi/ok`).
- `packages` — package-manager health: `dpkg --audit`, `apt-get check`, `dnf check`, `pacman -Dk` (`packages/broken`, `packages/locked`, `packages/ok`).
- `flatpak/unused-runtimes` — `flatpak uninstall --unused --dry-run` for stale SDKs.
- **Documentation:** `docs/checks.md` — auto-generated catalog of all 44 checks and 145 codes (`scripts/generate-check-docs.mjs`).
- **Man page:** `packaging/linux-doctor.1` now documents every flag, including `--history-json`, `--thresholds-set`, `--alert`, `--daemon`, and `--interval`.
- **Thresholds:** `inodeFullPct` / `inodeWarnPct` documented in `docs/configuration.md` and `DEFAULT_THRESHOLDS`.
### Changed
- **Safer `--fix` catalog — no more accidental SSH drops.** `network/no-route` (interface down/up cycle) is now `manual` tier — it would have killed the SSH session running `--fix --yes`. On Debian, `security/no-firewall` now runs `sudo ufw allow OpenSSH` *before* `sudo ufw --force enable` (plus `--force` to avoid the interactive y/n hang; existing sessions survive via conntrack).
- **Threshold validation is now strict.** `loadThresholds` and both `POST /api/thresholds` handlers drop non-numeric values (`"90%"` → ignored, keeps default) instead of storing `NaN` and breaking comparisons.
- **Scrubbing now covers compressed IPv6.** `scrub()` also redacts `::1` and `fe80::` without breaking `12:34:56` timestamps (guarded by `::` / `[A-Fa-f]`). Used by both support bundles and `--ai`.
### Fixed
- **Processes: header row no longer shifts the top-3.** `ps -o rss` leaves a `RSS` header even with `args=`; the parser now filters it explicitly. `processes/ok` correctly shows the real top consumers.
## [0.3.4] - 2026-08-26
### Security
- **Desktop: harden the loopback report API against cross-origin access**: the
shell's report server answered every request with
`Access-Control-Allow-Origin: *`, so while the app was running any website
open in the user's browser could read the full system report from
`http://127.0.0.1:17321/` and POST to the config-writing endpoints
(`/thresholds`, `/api/ignore`). It now applies the same rules as the CLI
dashboard server (`src/web.js`): loopback `Host` headers only (anti
DNS-rebinding), writes accepted only without an `Origin` header or from our
own origins (`tauri://localhost`, loopback), and the CORS wildcard replaced
by a strict origin echo — public sites can no longer read the report.
Covered by new unit tests on the guard helpers.
- **Tauri CSP**: `tauri.conf.json` `csp: null` → strict `default-src 'self' ipc: http://ipc.localhost; script-src 'self' 'unsafe-inline'; ...` — no wildcard.
### Added
- **Release: all distros** — `release.yml` now builds `AppImage + deb + rpm + tgz` (was `appimage,deb` only) so one tag covers Debian/Ubuntu (deb), Fedora/RHEL/Bazzite (rpm), and every glibc distro (AppImage) plus `npm` (tgz).
- **Docs: dashboard screenshots** — `docs/screenshots/dashboard-light.png` + `dark.png` regenerated for the fast compact UI (`codepill` + `durpill`, `Export ▾` dropdown, `186K/191K`) via Playwright on the live `0.3.4` dashboard.
- **Expanded safe-fix catalog (22 codes)**: `containerdisk/high|warn` (prune), `security/no-firewall` (family-aware), `firmware/pending`, `locales/broken`, `disk/full`, `suspend/failed`, plus `network/no-route|dns|dns-slow`, `ssh/root-login|root-password`, `bluetooth/failed|stopped`, `ntp/pending|unsynced`, `thermal/*`, `hardware/mce|ecc`, `security/autologin`, `backup/none`. All pinned by `fix-catalog.test.js`.
- **`--history-clear`**: clear stored run history (`src/history.js` `clearHistory()`), with completions and `--help` entry.
- **Dashboard: fast compact UI for technical users** — compact density by default, `codepill` on every card (click to copy, `code:` filter in search with 60ms debounce), `durpill` per check (from `durations` now always in `/api/report`), `Export ▾` dropdown, `skel-hero` skeletons, header blur, gauge 84px, card tint. `432 tests` still pass.
- **Man page 0.3.4**: `packaging/linux-doctor.1` now documents every flag (`--fix`, `--interactive`, `--notify`, `--support`, `--no-history`, `--history-clear`, etc.).
- **CI: Rust tests**: `ci.yml` now runs `cargo test` in `src-tauri` with webkit deps.
### Changed
- **Thresholds**: `dnsSlowMs` (500ms) now configurable via `config.json` and documented in `docs/configuration.md`; `containerWarnGB/HighGB` documented.
- **Tauri Node fallback**: `src-tauri/src/lib.rs` `node_bin()` now tries `LINUX_DOCTOR_NODE`, `runtime/node|nodejs`, `node` then `nodejs` on PATH, with `which()` helper and detailed error (suggests `LINUX_DOCTOR_NODE=nodejs` on Debian).
### Fixed
- **Completions drift**: `completions/*.bash|zsh|fish` now include `--history-clear` (caught by `completions.test.js`).
- **Web test**: `web.test.js` still matches `Re-run checks` after header refresh.
## [0.3.3] - 2026-08-26
### Fixed
- **Desktop (AppImage): EGL launch aborts on bleeding-edge Mesa**: the shell
now defaults to software GL (`LIBGL_ALWAYS_SOFTWARE=1`) when running from
an AppImage, set before any GTK/WebKit initialization. The bundled
WebKitGTK's accelerated paths could abort at window creation against very
new host Mesa — on AMD and Intel alike, since both run on Mesa (NVIDIA's
proprietary driver ships its own stack). A diagnostics dashboard does not
need GPU acceleration; opt out with `LINUX_DOCTOR_HARDWARE_GL=1`. The
`.deb` and CLI are unaffected — they use the host's matching WebKitGTK.
This retires the manual `LIBGL_ALWAYS_SOFTWARE=1` workaround documented
for v0.3.2.
### Changed
- **Dashboard: first-run history explainer**: with fewer than two recorded
runs, the History section no longer vanishes silently — it shows what the
one run on record scored and what will appear there after the next check
(static `--html` exports keep the section hidden, as before).
- **Dashboard: motion and number polish**: JS smooth-scrolls (sidebar jump,
checks-matrix jump, START HERE jump, history diff) now honor
`prefers-reduced-motion`; live numbers (NEW/FIXED badges, score delta,
trend title) use tabular figures so they stop jittering as counts change;
the gauge ring eases between severity colors instead of snapping; the hero
delta on a first run reads "no baseline yet" instead of "no history to
compare".
- **Dashboard: the subtraction pass**: removed the ornamental CSS layer
(decorative gradients, noise texture, glassmorphism, glowing gauge, stagger
card animations, hover lift) and consolidated everything into one calm
layer. Findings render as a single readable column with neutral info rows;
the healthy state gets a visually lighter card with a green hairline;
spacing follows a strict rhythm (header 40 / status 36 / toolbar 28 /
history 56); font stacks are honest system fonts. No libraries, no new
features; the parity vocabulary is untouched.
- **JSON schema documents the whole payload (Phase 5)**: `--schema` now
covers every field the runtime emits — `nextAction`, `cleanStreak`,
`scoreDelta`, `previousScore`, `lastRunAt`, top-level `unchanged`,
`scoreBreakdown`, `durations` (`--profile`), and `system.osRelease`.
schemaVersion stays **1**: the documented policy is that additive optional
fields never bump the version; incompatible changes do. Consumers should
ignore unknown properties.
- **Open-core split (Pro is no longer in this repository)**: the five premium
checks, all key-signing crypto, and `--license-gen` have moved to the
separate proprietary package `@linux-doctor/pro`, distributed only through
private channels (GitHub Packages buyer tokens or signed downloads). The
free edition now discovers an installed Pro module automatically
(`@linux-doctor/pro`, or `LINUX_DOCTOR_PRO_MODULE=/path/index.mjs` for
air-gapped machines) and injects its core primitives via
`init(core) → { checks, licensing }`. Consequences:
- Pro is genuinely unavailable for free — no public secret, no self-minted
keys; a key string alone unlocks nothing.
- The free repo carries zero licensing code; a new guard test
(`tests/open-core.test.js`) fails the build if any of it ever reappears,
and pins the loader contract with a fixture module.
- Without the add-on, behavior is identical whether or not a key string is
configured ("Linux Doctor Pro: not installed").
### Added
- **Output drift guard (Phase 5)**: `tests/output-drift.test.js` runs the real
binary and asserts every emitted key — top level and in the nested
`system` / `diffSinceLast` / finding / scoreBreakdown objects — is
documented in `src/schema.js`. A new payload field without a schema entry
now fails CI instead of surprising scripts and fleet consumers.
- **Golden output snapshots (Phase 5)**: the four states from the parity
matrix (mixed / healthy-streak / info-only / first-run) are snapshotted for
pretty, `--plain`, and `--json` in `tests/golden/snapshots/` and compared
byte-for-byte. Regeneration is controlled (`npm run goldens:update`) so any
wording or ordering change shows up as a reviewable diff.
- **Score breakdown everywhere (Phase 5)**: the health score's arithmetic is
now auditable — `scoreBreakdown` travels in `--json`, the terminal report
prints one compact line under STATUS (`SCORE 77/100 = 100 −15 disk/full …`),
and `--plain` carries `# score-breakdown:`. The score is derived from the
breakdown by construction (`100 − Σpenalty === score`, pinned by tests).
- **Fix catalog ↔ registry pin (Phase 5)**: every safe-fix catalog code must
exist in the finding-code registry, and each entry must still produce a
plan for its own finding shape — a typo'd catalog key fails at test time.
- **Output parity contract (Faza 4)**: [docs/output-parity.md](docs/output-parity.md)
documents the one-message-four-channels rule (CLI / `--plain` / `--json` /
dashboard) with a per-state verification matrix. The vocabulary is pinned
by tests that grep both the CLI output and the dashboard HTML; a new test
guarantees `nextAction` in `--json` always matches the report's ▶ START
HERE line.
- **`nextAction` in the JSON payload**: the ▶ START HERE pick now travels as
data (`{code, severity, title, fix}`), and the dashboard renders it
directly instead of recomputing — banner and report can never disagree
(older payloads keep the client-side fallback).
- **"Failed checks" in the dashboard**: checks that threw are now visible in
the GUI with the same wording as the CLI, so a partially-broken run is
never silently rendered as a clean one.
- **Ignore management from the CLI (Faza 3)**: `--ignore-add <value>` /
`--ignore-remove <value>` write the same config keys as the dashboard's
Ignore button. Code-shaped values (`check/reason`) land in the
exact-match code list; anything else is a title fragment. Both commands
print the resulting list using the same format as `--ignore-list` (one
vocabulary everywhere).
- **Healthy-state streak**: when a machine is clean, the report says so with
momentum — "✅ Everything is clean — N clean run(s) in a row" — computed
from history and exposed to the dashboard via `cleanStreak` in the JSON
payload. Info-only systems read "No high or medium issues — N
informational notes below" instead of pretending there is nothing.
- **Support bundle hardening**: `diffSinceLast` titles now go through the
same redaction (IPs, home paths) as finding text.
### Changed
- **History v2 (Faza 2 — history as the core)**:
- The file wrapper now carries `version: 2` (v1 files still read fine).
Every saved finding is guaranteed to have its stable `code`; diffing
among coded entries is code-only, so rewritten titles can no longer
churn NEW/FIXED markers.
- **Repair-on-read**: a partially corrupted history keeps its well-formed
runs and drops only the broken entries (and broken finding rows inside
them); a truncated file degrades to "no history" instead of failing.
- **Upgrade bridge**: v1 entries stored without codes are matched by
title for their remaining lifetime in the window, so upgrading never
reads as everything-new-plus-everything-fixed at once. Reworded issues
surface exactly once during the transition, then identity is permanent.
- New `tests/output-parity.test.js` pins the one-computation contract:
CLI text, `--plain` and `--json` (and therefore the dashboard) must
render identical new/fixed/unchanged numbers from the same diff object,
and the score stays severity-driven regardless of history content.
- **Severity rubric enforced (Faza 1 — findings trust)**: new
[docs/severity.md](docs/severity.md) defines when a finding is high /
medium / info, and `tests/codes-registry.test.js` pins every built-in code
to its allowed severity set + category. Four findings were realigned with
the rubric — degradation without data risk is **medium**, not high:
`gpu/software-rendering`, `wayland/software-rendering`,
`gpu/nvidia-missing`, `processes/high`. Machines that scored on these will
see their health score improve; history keeps working (scores are
recomputed per run).
- **Code discipline is mechanical**: every builtin `code:` must be a string
literal (`^[a-z0-9-]+/[a-z0-9-]+$`, ternaries of literals allowed), titles
carry no trailing period and stay short, and `evidence: null` is only
allowed on the reviewed data-absence list (`battery/none`, `gpu/skipped`,
`updates/skipped`). The slug-derived fallback code now exists solely as a
plugin escape hatch.
- **Health score escalation**: penalties now compound within a severity tier —
the n-th high-severity finding costs 15 + 5·(n−1), and medium findings past
the third cost +1 each. Previously 4 high (score 40) and 7 medium (44)
scored almost identically; now they land at 10 vs 34, so critical issues
stand clearly apart from a pile of minor ones. Low counts are unchanged
(one high is still 85, one medium still 92).
- **`--push` / `--alert` URL validation**: mistyped endpoints (missing
`https://`, wrong scheme, no host) now fail fast with a clear message and
exit 2, before any checks run — instead of a generic fetch error after a
full run or mid-daemon-cycle.
- **Consistent repository URLs**: every reference to the old
`linux-doctor-cli` repo name (package.json, PKGBUILD, RPM spec, report
footer, GUI "Report wrong" link, commercial license) now points to
`7sh1d0w7x/linux-doctor`.
- **Distro-specific fixes**: `pkgInstall()` accepts per-distro package-name
maps, and the `memory`, `network` and `smart` checks now emit the install
command for the detected distro instead of enumerating alternatives.
### Added
- **Safe fixes (`--fix`)**: a dry-run plan of concrete commands for the
findings on the system, built from a small built-in catalog (`src/fix.js`)
mapped to stable finding codes — failed services, broken timers, pending
updates (per package family, `rpm-ostree` on image-based systems), flatpak,
snap, disabled fstrim, oversized journal. `--fix --yes` executes only the
`[apply]` commands after a second opt-in; `[manual]` entries (e.g. reboot)
are never executed.
- **Interactive mode (`--interactive`)**: zero-dependency terminal UI over the
report — arrow keys pick a finding, Enter opens details/evidence, `c` copies
the fix (wl-copy/xclip/pbcopy with OSC 52 fallback), `f` copies the safe-fix
command. Falls back to the printed report without a TTY.
- **Desktop notifications (`--notify`)**: native `notify-send` notification
when new medium/high findings appear; also honored by `--daemon`.
- **Score trend in the terminal**: a `TREND` sparkline (`▄▅▆▇`) of recent runs
(capped to 20) in both the full report and `--plain` (`# trend:` comment).
- **▶ START HERE**: the report now calls out the single most useful next
action between the summary and the findings. The web dashboard and static
HTML export get the same banner with Copy-fix / Show buttons.
- **Category clustering**: findings are grouped by their check's category
within each severity section, each numbered line carrying a `[category]`
tag.
- **Shell-injection guardrail test**: a static test (`tests/shell-safety.test.js`)
fails the suite if any command template interpolates a value without `shq()`,
so the existing quoting discipline is enforced mechanically.
- **`npx github:7sh1d0w7x/linux-doctor`**: documented zero-install trial path
(the corrected repo URLs make it work).
- **Checks**: `fstrim` (SSD TRIM health — weekly `fstrim.timer`, continuous
`discard` mounting, or a medium finding when SSDs are never trimmed).
### Changed
- **Security hardening** (web dashboard): non-loopback `Host` headers are
rejected (DNS rebinding) and cross-origin POSTs to config-writing endpoints
are rejected (CSRF).
- **Static HTML reports** embed data as `window.__DATA__` instead of
monkey-patching `window.fetch`; save buttons now fail honestly in static files.
- **Shell safety**: every value interpolated into a spawned command is
single-quoted (`shq`) so crafted names cannot break out of their argument.
### Fixed
- **TREND no longer lags a run behind**: the sparkline now ends at the
current run's score (previously it showed only stored runs, so a fresh
recovery read as a continuing decline). The window still caps at 20
points, current run included. Pinned by `output-parity.test.js`.
- **Score delta is visible in the terminal report**: the pretty STATUS line
now carries the same "(+N)" convention as `--plain`
(`health 76/100 (+9)`), so recovery reads identically in every channel.
- **Dashboard Auto-refresh paused state is visible**: the button previously
kept its active styling while paused; it now renders amber/dimmed.
## [0.3.2] - 2026-08-25
### Added
- **Desktop: complete loopback API**: the shell's report server now answers
`/checks` (check → category map: category grouping and the checks matrix
work in the installed app), `/history` (score trend section), and
`GET/POST /thresholds` plus `POST /api/ignore` (thresholds panel and the
per-finding Ignore button persist). Backed by three new hidden CLI flags
(`--history-json`, `--thresholds-json`, `--thresholds-set`) so the Rust
shell keeps delegating all logic to Node.
### Fixed
- **Desktop (AppImage): empty report**: the AppRun environment pointed
`LD_LIBRARY_PATH` at the bundled libraries, which made the host's `node`
abort with a symbol/version error — `/report` came back with an empty
body. The shell now strips `LD_LIBRARY_PATH`/`LD_PRELOAD` from every Node
child process.
## [0.3.0] - 2026-08-18
### Added
- **Checks**: `containerdisk` (container image storage via podman/docker),
`crash` (reboot frequency + coredumps via journalctl and coredumpctl).
- **Shell completions**: bash, zsh, and fish completions in `completions/`.
- **Systemd timer**: `linux-doctor.timer` + `.service` for automated daily
checks (see [packaging/](packaging/)).
- **HTML export**: `--html <path>` saves a standalone HTML report you can
open in any browser or share on Slack.
- **`--summary`**: one-liner output with score + severity counts, designed
for cron jobs and shell prompts (`score=72 high=1 medium=3 info=12`).
- **`--init-config`**: creates a starter config file at
`~/.config/linux-doctor/config.json` with commented thresholds and
examples — no more guessing what keys exist.
- **`--check-list`**: prints check metadata as JSON (id, title, category,
appliesTo, appliesHere) — useful for dashboards and fleet tooling.
- **`--compare <file>`**: diffs a previous JSON report against the current
run, showing new and fixed findings side by side.
- **DNS speed check**: the `network` check now times DNS resolution; slow
resolvers (>500ms) are flagged as medium severity.
- **Man page**: `linux-doctor(1)` with full option docs, exit codes, and
file locations (see `packaging/linux-doctor.1`).
- **`--severity <level>`**: filter output to show only high, medium, or info
findings (scoring and fleet push always use the full set).
- **`--ignore-code <code>`**: ignore findings by stable code (e.g.
`services/failed`) instead of fragile title substrings; works alongside
`--ignore` and the config file's `ignoreCodes` array.
- **`--ignore-list`**: shows all configured ignore patterns (title + code)
and exits.
- **Terminal colors**: severity headers are now ANSI-colored (red/yellow/blue)
when stdout is a TTY.
- **NEW badge**: findings new since the last check now show `🆕 NEW` instead
of the subtle `(new)` marker.
- Dashboard: a **Report wrong** button on every finding opens a pre-filled
GitHub issue (title, code, check, version) — the false-positive feedback
loop.
- CI: a **real-run** job executes the full `--json` report on a live ubuntu
runner and asserts valid JSON with zero `checkErrors`; the Fedora container
job runs the same smoke test against its real `/proc` and `/sys`.
- Journal findings are flagged `low` confidence (heuristic log matching may
produce false positives).
#### History, support bundle, atomic awareness, dashboard trend
- **History is now central**: every run is diffed against the previous one and
reports `newCount` / `fixedCount` / `unchanged` plus a plain-language
`changeMessage` ("Since last run: 2 new, 1 fixed", or "no change since last
check"). `--summary` and the JSON report carry these; `--no-history` (or
`LINUX_DOCTOR_NO_HISTORY`) disables history recording for a one-off run.
- **`--support`**: writes a single, privacy-scrubbed support bundle
(`linux-doctor-support-<timestamp>.json`) with system facts, the active
config, recent findings, and a capped copy of history. IP addresses and
`/home/<user>` paths are redacted; the bundle documents exactly what it
excludes under `privacy.excluded`.
- **Immutable / atomic distro awareness**: `system.atomic` reports
`immutable`, `imageBased`, `bootc`, `variant`, and `pkg`. Checks that do not
apply to image-based systems (e.g. `reboot`) are skipped and listed in
`skippedChecks` / `checksAtomicSkipped` (and surfaced in the dashboard's
Skipped section) instead of producing misleading findings.
- **Dashboard trend graphs**: the history view now draws the health-score
sparkline (with date tooltips and a dashed 50-point attention line) plus a
new stacked-bar chart of high/medium/info findings across recent runs.
### Changed
- **Repositioned messaging around the core promise** — finds real problems, explains them in plain English, and remembers what changed between runs. The README now leads with that promise (not a feature list); the terminal report and `--summary` lead with `STATUS` and a `SINCE LAST RUN` change line (new/fixed/unchanged); the `--plain` report gains a `# since:` line; and the dashboard hero surfaces "Since last run" next to the health score. No feature/behavior change to the checks themselves.
- **Stable finding codes**: every count-bearing finding (services, journal,
updates, flatpak, firmware, timers, disk, memory, network, ntp, audio,
thermal, smart, journald) now carries an explicit stable `code`, and the
history diff matches findings by code (falling back to title for older
history). "New/fixed since last check" no longer churns when a count in a
title changes between runs.
- **Explicit skips**: checks that cannot run because a tool is missing
(`free`, `journalctl`, `smartctl`, `timedatectl`, `ip`, `pactl`, a package
manager, thermal zones) now report an informational "check skipped"
finding instead of silently returning "all clear".
- **Dedupe**: disk findings share one `dedupeKey` per backing device, so
btrfs/ZFS subvolumes of the same pool are scored once; the journal check
defers unit-failure lines to the services check (one problem, one finding).
### Fixed
- Packaging: the PKGBUILD and RPM spec now build from the real `npm pack`
tarball layout (`package/`), which previously failed at `%setup`/`cp` time;
`diffSinceLast` entries expose the stable `code` in the v1 JSON schema.
- **NEW badge** printed the literal template string `${A.bold}` instead of the
ANSI code it was meant to reference — the badge now renders as actual colored
output.
- **Thermal** check matched any log line containing "throttl" — e.g. Roblox's
`setStartupThrottle` — and falsely reported CPU throttling. The pattern now
requires `clock throttl` and a kernel-tagged log source.
- **Journal** check escalated thousands of benign but unrecognized error-level
entries into a medium "noteworthy errors" finding. Noise patterns now cover
X.509 certs, ELF architecture, i2c address, rpm-ostree metadata, and
libseccomp lines; unrecognized entries are informational, never medium.
- **GPU** check reported a kernel-built-in driver (absent from `lsmod`) as
"driver not loaded" even with a working display — a built-in driver plus a
working display is now fine.
- **Updates** check could print "System is up to date" when `apt` actually
failed (dpkg lock, `E:` errors). Apt failures now stay silent instead of
pretending the system is clean.
- **Severity**: missing optional hardening — no LUKS encryption, Secure Boot
off, no TPM, no firewall, no backup tool/schedule — is informational, not
medium. A healthy default install is no longer penalized in the health score.
## [0.2.0] - 2026-08-18
### Added
- **Checks**: `network` (default route + DNS), `reboot` (newer kernel installed
but not booted, `/var/run/reboot-required`), `journald` (journal disk usage),
`smart` (SMART disk health, graceful without root), `timers` (enabled systemd
timers that never run), `ntp` (time synchronization), battery capacity wear.
- **JSON v1 schema**: `schemaVersion`, `tool`, `version`, `durationMs`;
every finding carries `check` and a stable `code`; `--schema` prints the
JSON Schema document for validators and fleet servers.
- **History diff**: atomic history writes; `diffSinceLast` (`added`/`fixed`)
in JSON and a "fixed since last check" line in the terminal and `--plain`
output.
- **Check metadata**: `defineCheck` with `category` and `appliesTo`
(desktop/laptop/server); `--list` and `--help` group checks by category and
mark checks that do not apply to the current machine; a full run skips
irrelevant checks (`--check` always overrides).
- **Plugins**: drop-in checks in `~/.config/linux-doctor/checks/`
(`LINUX_DOCTOR_PLUGINS` to override) — any `.js` file exporting
`{ id, run }` becomes a check.
- **Configurable thresholds**: `"thresholds"` in `config.json` tunes every
severity boundary (disk %, memory ratio, load, temperature, RAM share,
journal size).
- **Performance**: checks run through a concurrency pool (bounded
subprocesses); `--profile` reports per-check durations.
- **CLI hygiene**: strict argument parsing (unknown flags and missing values
exit 2), `--check=id` form, `--push` payload now carries `machineId` and
`diffSinceLast`.
- **Packaging**: AUR `PKGBUILD`, RPM spec, release workflow, this changelog.
- **CI**: Node 20/22/24 matrix, a Fedora container job, and an npm pack gate
that guards the published `files` list.
### Fixed
- npm package crashed on every invocation: `src-gui/index.html` (read by
`web.js` at import time) is now in `files`.
- Same root cause reported twice (journal+suspend, gpu+wayland) and double
penalized in the health score — now deduplicated via `dedupeKey` and
deferred patterns.
- `ps -o comm` truncated process names (15 chars) so the friendly-name map
never matched — now uses `args=`.
- `journalctl` boot separators inflated log checks — filtered in one place.
- `addIgnore` used to overwrite other config keys (e.g. thresholds) — it
merges now.
- Wireless-device "batteries" (Logitech receivers, controllers) no longer
report as laptop batteries or mark a desktop as a laptop.
- `systemd list-timers` column parsing was fragile — now slices by the
header's column offsets.
- `tests/web.test.js` flaked (server banner corrupted the TAP stream) —
`startWeb` gained a `quiet` option used by tests.
[Unreleased]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.8.3...HEAD
[0.8.3]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.8.2...v0.8.3
[0.8.2]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.7.3...v0.8.0
[0.7.3]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/7sh1d0w7x/linux-doctor/compare/v0.3.5...v0.4.0