Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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