feat(doctor): battery health readout (#80)
Some checks failed
Check / eval (push) Has been cancelled
Some checks failed
Check / eval (push) Has been cancelled
Report-only cycle_count + retained capacity (charge_* or energy_*) as % of design on system batteries; soft warn below 70% of design; skip when attrs missing. NOMARCHY_POWER_SUPPLY_ROOT for the checks.doctor fixture (test_power has no cycle/design attrs). Verified: V2 — nix flake check --no-build; checks.doctor green (fake BAT0 cycles/% + Device-scope ignore); local BAT0 smoke (energy_*).
This commit is contained in:
@@ -55,11 +55,6 @@ runtime-computed `resume_offset` into the boot cmdline, and the dev box runs
|
|||||||
exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in
|
exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in
|
||||||
HARDWARE-QUEUE. Close #76 on PASS.
|
HARDWARE-QUEUE. Close #76 on PASS.
|
||||||
|
|
||||||
### 80. Battery health readout (`VISION § C`)
|
|
||||||
Report-only: `cycle_count` + `charge_full`/`charge_full_design` % in a
|
|
||||||
doctor battery-health line (and optionally the Power menu). sysfs-gated
|
|
||||||
so desktops/VMs skip it. Extend checks.doctor with a faked sysfs battery.
|
|
||||||
|
|
||||||
### 81. First-boot welcome card (`VISION § B`)
|
### 81. First-boot welcome card (`VISION § B`)
|
||||||
One dismissible "you're set" toast on the first session (SUPER+M menu,
|
One dismissible "you're set" toast on the first session (SUPER+M menu,
|
||||||
SUPER+T themes, SUPER+? keys, network pointer) — a notification, **not**
|
SUPER+T themes, SUPER+? keys, network pointer) — a notification, **not**
|
||||||
@@ -145,9 +140,9 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
|||||||
2026-07-10). Keep #59 commented install guidance; no
|
2026-07-10). Keep #59 commented install guidance; no
|
||||||
`nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue.
|
`nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue.
|
||||||
|
|
||||||
_(First-boot card, generation readability, battery health, pre-activate
|
_(#80 battery health shipped 2026-07-11. Remaining promoted: first-boot
|
||||||
flag, dock life, hall of fame → promoted to **NEXT #80–#85**,
|
card, generation readability, pre-activate flag, dock life, hall of
|
||||||
Bernardo 2026-07-11.)_
|
fame → **NEXT #81–#85**.)_
|
||||||
|
|
||||||
### v1.0 pointer
|
### v1.0 pointer
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ Template:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-11 — #80 doctor battery health readout
|
||||||
|
- **Task:** NEXT #80 — battery health in `nomarchy-doctor` (`VISION § C`).
|
||||||
|
- **Did:** Doctor row: `cycle_count` + retained capacity
|
||||||
|
(`charge_full`/`design` or `energy_*`) as `% of design`; system
|
||||||
|
batteries only; soft warn <70% design; skip when attrs absent.
|
||||||
|
`NOMARCHY_POWER_SUPPLY_ROOT` for the VM fixture. Extended
|
||||||
|
`checks.doctor` (fake BAT0 + Device-scope MOUSE0 ignore). HARDWARE §10,
|
||||||
|
ROADMAP ✓, VISION ✓; closed #80.
|
||||||
|
- **Verified:** **V2** — `nix flake check --no-build`; `checks.doctor`
|
||||||
|
green; local smoke on real BAT0 (342 cycles, 81% via energy_*) + fixture.
|
||||||
|
- **Pending:** Power-menu surface left optional/out of scope (doctor is
|
||||||
|
the product surface for report-only health).
|
||||||
|
- **Next suggestion:** #81 first-boot welcome card (highest product ROI)
|
||||||
|
or #85 docs-only hall of fame.
|
||||||
|
|
||||||
## 2026-07-11 — Triage: VISION-gap PROPOSED → NEXT #80–#85 (Bernardo)
|
## 2026-07-11 — Triage: VISION-gap PROPOSED → NEXT #80–#85 (Bernardo)
|
||||||
- **Task:** Bernardo promoted five of the six fresh VISION-gap pitches
|
- **Task:** Bernardo promoted five of the six fresh VISION-gap pitches
|
||||||
(all but the wallpapers split, which stays LATER).
|
(all but the wallpapers split, which stays LATER).
|
||||||
|
|||||||
@@ -378,11 +378,10 @@ For contributors (and power users who will PR):
|
|||||||
## 10. Doctor and hardware health (current vs target)
|
## 10. Doctor and hardware health (current vs target)
|
||||||
|
|
||||||
**Today** (`nomarchy-doctor`): failed units, disk space, theme-state
|
**Today** (`nomarchy-doctor`): failed units, disk space, theme-state
|
||||||
validity/git, generation age, snapper timer. **No** Wi‑Fi, GPU, fwupd,
|
validity/git, generation age, snapper; hardware section self-gates per
|
||||||
or fingerprint checks.
|
machine (NetworkManager, audio sink, GPU smoke, fprintd, fwupd, charge
|
||||||
|
limit, battery health, hibernate/zram). All read-only; each ✖ prints one
|
||||||
**Target checks** (queued, all read-only, each failure prints one fix
|
fix command.
|
||||||
command — same doctor contract):
|
|
||||||
|
|
||||||
| Check | Pass condition | Suggested fix line |
|
| Check | Pass condition | Suggested fix line |
|
||||||
|-------|----------------|--------------------|
|
|-------|----------------|--------------------|
|
||||||
@@ -391,7 +390,9 @@ command — same doctor contract):
|
|||||||
| GPU accel | `glxinfo`/`vainfo` smoke (if installed) | check `hardwareProfile` / drivers |
|
| GPU accel | `glxinfo`/`vainfo` smoke (if installed) | check `hardwareProfile` / drivers |
|
||||||
| Fingerprint | if USB VID matched / fprintd unit | `fprintd-enroll` or enable option |
|
| Fingerprint | if USB VID matched / fprintd unit | `fprintd-enroll` or enable option |
|
||||||
| fwupd | daemon active; optional “updates pending” warn | `fwupdmgr get-updates` |
|
| fwupd | daemon active; optional “updates pending” warn | `fwupdmgr get-updates` |
|
||||||
| Battery threshold | if laptop + limit set, sysfs writable | power docs |
|
| Battery threshold | if laptop + limit set, sysfs reports it | power docs / restart charge-limit unit |
|
||||||
|
| Battery health | cycle_count and/or charge\|energy_full÷design % when sysfs exports them | report-only (warn if <70% of design) |
|
||||||
|
| Hibernate / zram | disk swap + resume=; zram active | docs/MIGRATION.md → Enabling hibernation |
|
||||||
|
|
||||||
## 11. Option quick reference
|
## 11. Option quick reference
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,12 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
`checks.doctor` VM test asserts the `hibernate` row renders. (Gotcha
|
`checks.doctor` VM test asserts the `hibernate` row renders. (Gotcha
|
||||||
recorded: the script runs `set -euo pipefail`, so a no-match `grep` inside
|
recorded: the script runs `set -euo pipefail`, so a no-match `grep` inside
|
||||||
`$(…)` or a `cond && action` aborts it — use `|| true` and `if`.)
|
`$(…)` or a `cond && action` aborts it — use `|| true` and `if`.)
|
||||||
|
✓ **Doctor battery health (#80, `VISION § C`):** report-only
|
||||||
|
`cycle_count` + retained capacity (`charge_full`/`charge_full_design` or
|
||||||
|
`energy_*`) as `% of design`, system batteries only (not Device-scope
|
||||||
|
peripherals). Self-gates when attrs missing; soft warn below 70% of
|
||||||
|
design. `NOMARCHY_POWER_SUPPLY_ROOT` for the `checks.doctor` fixture
|
||||||
|
(`test_power` has no cycle/design attrs).
|
||||||
- ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS
|
- ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS
|
||||||
passphrase already gates the machine, but locking hyprlock before sleep
|
passphrase already gates the machine, but locking hyprlock before sleep
|
||||||
meant a second password. Fixed by *not locking* before an encrypted
|
meant a second password. Fixed by *not locking* before an encrypted
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §10.
|
|||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| **Dock life** | Lid closed + external only, wake, default audio sink on undock | Display profiles exist — polish edge cases |
|
| **Dock life** | Lid closed + external only, wake, default audio sink on undock | Display profiles exist — polish edge cases |
|
||||||
| **Hibernate/sleep confidence** | Doctor: resume device, swap size, clean suspend journal | Trust > new power UI |
|
| **Hibernate/sleep confidence** | Doctor: resume device, swap size, clean suspend journal | Trust > new power UI |
|
||||||
| **Battery health readout** | Cycles / charge limit where sysfs allows | Report-only |
|
| **Battery health readout** ✓ | Cycles + design capacity % in doctor where sysfs allows | ✓ shipped #80 (report-only; charge limit is a separate row) |
|
||||||
| **Charge-limit instant apply** | Already PROPOSED `[blocked:hw]` | Keep privilege tradeoff explicit |
|
| **Charge-limit instant apply** | Already PROPOSED `[blocked:hw]` | Keep privilege tradeoff explicit |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
28
flake.nix
28
flake.nix
@@ -428,8 +428,34 @@
|
|||||||
# sheet must still exit 0 — i.e. the section runs without
|
# sheet must still exit 0 — i.e. the section runs without
|
||||||
# erroring under `set -euo pipefail`.
|
# erroring under `set -euo pipefail`.
|
||||||
healthy = machine.succeed("nomarchy-doctor 2>&1")
|
healthy = machine.succeed("nomarchy-doctor 2>&1")
|
||||||
for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API", "hibernate"]:
|
for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API", "hibernate", "battery health"]:
|
||||||
assert probe in healthy, f"doctor hardware section missing a self-gated '{probe}' row:\n{healthy}"
|
assert probe in healthy, f"doctor hardware section missing a self-gated '{probe}' row:\n{healthy}"
|
||||||
|
# #80 battery health: faked sysfs (test_power has no cycle/design).
|
||||||
|
machine.succeed(
|
||||||
|
"mkdir -p /tmp/fake-ps/BAT0 && "
|
||||||
|
"echo Battery > /tmp/fake-ps/BAT0/type && "
|
||||||
|
"echo System > /tmp/fake-ps/BAT0/scope && "
|
||||||
|
"echo 150 > /tmp/fake-ps/BAT0/cycle_count && "
|
||||||
|
"echo 4000000 > /tmp/fake-ps/BAT0/charge_full && "
|
||||||
|
"echo 5000000 > /tmp/fake-ps/BAT0/charge_full_design"
|
||||||
|
)
|
||||||
|
bat = machine.succeed(
|
||||||
|
"NOMARCHY_POWER_SUPPLY_ROOT=/tmp/fake-ps nomarchy-doctor 2>&1"
|
||||||
|
)
|
||||||
|
assert "BAT0 health" in bat, f"doctor missing battery health line:\n{bat}"
|
||||||
|
assert "150 cycles" in bat, f"doctor missing cycle_count:\n{bat}"
|
||||||
|
assert "80% of design capacity" in bat, f"doctor missing design %:\n{bat}"
|
||||||
|
# Peripheral Device-scope batteries must not produce a health line.
|
||||||
|
machine.succeed(
|
||||||
|
"mkdir -p /tmp/fake-ps/MOUSE0 && "
|
||||||
|
"echo Battery > /tmp/fake-ps/MOUSE0/type && "
|
||||||
|
"echo Device > /tmp/fake-ps/MOUSE0/scope && "
|
||||||
|
"echo 9 > /tmp/fake-ps/MOUSE0/cycle_count"
|
||||||
|
)
|
||||||
|
bat2 = machine.succeed(
|
||||||
|
"NOMARCHY_POWER_SUPPLY_ROOT=/tmp/fake-ps nomarchy-doctor 2>&1"
|
||||||
|
)
|
||||||
|
assert "MOUSE0" not in bat2, f"doctor reported Device-scope battery:\n{bat2}"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,61 @@ if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then
|
|||||||
skip "battery charge limit (none set — charges to 100%)"
|
skip "battery charge limit (none set — charges to 100%)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── battery health (report-only, BACKLOG #80) ────────────────────────
|
||||||
|
# cycle_count + retained capacity (charge_* µAh or energy_* µWh). Same
|
||||||
|
# system-battery filter as charge-limit/notify. Self-gates when no
|
||||||
|
# battery or the firmware omits the attrs (desktops, bare VMs). Override
|
||||||
|
# the sysfs root with NOMARCHY_POWER_SUPPLY_ROOT for the checks.doctor
|
||||||
|
# fixture (test_power has no cycle/design attrs).
|
||||||
|
ps_root="${NOMARCHY_POWER_SUPPLY_ROOT:-/sys/class/power_supply}"
|
||||||
|
bat_health_seen=0
|
||||||
|
for bat in "$ps_root"/*/; do
|
||||||
|
[ -d "$bat" ] || continue
|
||||||
|
[ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue
|
||||||
|
[ "$(cat "$bat/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||||||
|
name=$(basename "$bat")
|
||||||
|
|
||||||
|
cycles=$(cat "$bat/cycle_count" 2>/dev/null || true)
|
||||||
|
case "$cycles" in ''|*[!0-9]*) cycles= ;; esac
|
||||||
|
# Some firmwares export 0 forever — treat as unknown, not "brand new".
|
||||||
|
if [ -n "$cycles" ] && [ "$cycles" -eq 0 ]; then cycles=; fi
|
||||||
|
|
||||||
|
full=""; design=""
|
||||||
|
if [ -r "$bat/charge_full" ] && [ -r "$bat/charge_full_design" ]; then
|
||||||
|
full=$(cat "$bat/charge_full" 2>/dev/null || true)
|
||||||
|
design=$(cat "$bat/charge_full_design" 2>/dev/null || true)
|
||||||
|
elif [ -r "$bat/energy_full" ] && [ -r "$bat/energy_full_design" ]; then
|
||||||
|
full=$(cat "$bat/energy_full" 2>/dev/null || true)
|
||||||
|
design=$(cat "$bat/energy_full_design" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
case "$full" in ''|*[!0-9]*) full= ;; esac
|
||||||
|
case "$design" in ''|*[!0-9]*) design= ;; esac
|
||||||
|
pct=""
|
||||||
|
if [ -n "$full" ] && [ -n "$design" ] && [ "$design" -gt 0 ]; then
|
||||||
|
pct=$(( full * 100 / design ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$cycles" ] || [ -n "$pct" ] || continue
|
||||||
|
bat_health_seen=1
|
||||||
|
|
||||||
|
detail=""
|
||||||
|
[ -n "$cycles" ] && detail="${cycles} cycles"
|
||||||
|
if [ -n "$pct" ]; then
|
||||||
|
[ -n "$detail" ] && detail="$detail, "
|
||||||
|
detail="${detail}${pct}% of design capacity"
|
||||||
|
fi
|
||||||
|
# Soft warn only — wear isn't a doctor "fix", just a heads-up.
|
||||||
|
if [ -n "$pct" ] && [ "$pct" -lt 70 ]; then
|
||||||
|
warn "$name health: $detail" \
|
||||||
|
"battery wear is normal over years — replace when runtime suffers"
|
||||||
|
else
|
||||||
|
ok "$name health: $detail"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$bat_health_seen" -eq 0 ]; then
|
||||||
|
skip "battery health (no system battery / no cycle or design capacity attrs)"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── hibernate / sleep (BACKLOG #76) ──────────────────────────────────
|
# ── hibernate / sleep (BACKLOG #76) ──────────────────────────────────
|
||||||
# Read-only: is there a working hibernate path? zram is RAM-only and can't
|
# Read-only: is there a working hibernate path? zram is RAM-only and can't
|
||||||
# hold a resume image, so hibernation needs a disk swap (partition or file)
|
# hold a resume image, so hibernation needs a disk swap (partition or file)
|
||||||
|
|||||||
Reference in New Issue
Block a user