aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
AgeCommit message (Collapse)AuthorFilesLines
2026-09-17docs: correct the claim about counting a symlinked directoryDanilo M.2-9/+16
GNU ls follows a symlink to a directory with or without a trailing slash, so both forms count the packages correctly. Only -d lists the link itself. The original claim came from a count taken where the path did not resolve at all, and it reached the spec, the plan and two commit messages. The count was always right; the reason given for it was not, and a comment that teaches the next reader something false is worse than no comment. Records the real trap in its place: ls aliased to a long format counts its "total" header as a package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: plan the network and slackware widgetsDanilo M.1-0/+2046
Fifteen tasks, TDD throughout: parsers and primitives first, then the samplers, then the cards, then the edits to existing widgets. Verified two assumptions the plan rests on rather than assuming them: Lua 5.4 provides utf8, so card.truncate can cut on a character boundary, and conky's Cairo bindings export cairo_get_line_width, without which the ring line-state fix has nothing to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: design the network and slackware widgetsDanilo M.1-0/+313
Two new cards and three fixes to existing ones. The network card reads the interface byte counters directly and keeps its own history, one sample per pixel column, so the chart never interpolates. Both series share one autoscale: drawn independently, a 200kB/s upload would look like a 40MB/s download. The public address moves from the old config's 5 minute poll to 30 minutes, since a residential address is stable for days. The slackware card takes its numbers from a sampler rather than the draw hook, and stores epochs instead of reconstructed dates. The shell function it replaces parses ls -l output and hardcodes the year, which breaks every January and on any file older than six months. Records two things found while checking real values against the design: /var/log/packages is a symlink, so counting it without a trailing slash reports 1 package rather than 2817, and the ChangeLog mtime on this machine currently reads in the future, so the age arithmetic has to survive a negative difference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: correct the system-widget comments and defer the age markerDanilo M.1-0/+12
Fix the conky.conf.in comment to name all three samplers and the README to say the cache sampler runs every 15 minutes. Rewrite the disks layout comment to match per_row = n (no wrapping), drop the stale "seventeenth bar" note, and record in the spec that the cache age marker is deliberately deferred with the epoch-line upgrade path.
2026-09-17docs: add the system widgets implementation planDanilo M.1-0/+1607
Eleven tasks. The palette's warning role first, since everything else colours against it; then an offscreen render harness, so each later task can look at its work without an install-restart-toggle cycle; then parsers, samplers, primitives, the four cards, and the live pass. Glyphs are taken from the old conky config and re-rendered rather than guessed. That matters: a guess at the Slackware mark drew a stomach, and the config had the real codepoint (U+F318) all along, along with the disk and network-share icons. The icon is the label now, so no mount name is drawn. The two NFS shares are told apart by their server's address, read from the mount's device field at runtime. The repo is public, so the address belongs on screen and not in a committed file; deriving it also means the card survives a renumbered LAN. df_parse keeps the device field for this, and `host` is non-nil exactly for a network mount. Two traps recorded because both would fail silently: df is aliased to df -h in the user's shell, so the sampler calls /usr/bin/df -P -B1 by absolute path or the parser reads '1.6G' where it expects bytes; and the per-core counters must outlive a frame, since load is a delta and a counter rebuilt each draw reports nil forever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: give the system widgets a shared colour languageDanilo M.1-12/+87
Thresholds colour every quantity that has a comfortable range: ok below the first, warning between, critical above. 25/75 for filesystems, cache and CPU load; per-sensor pairs for temperatures, because 70C is unremarkable on a CPU and alarming on an NVMe. The per-core row becomes an equaliser: sixteen vertical bars rising from a baseline, coloured by load. Disks become idea2's rings, an icon at the centre and the arc coloured by usage. Cache highlights its largest subdirectory. This needs a `warning` role the dashboard does not currently expose. udt-palette already resolves it in every scheme, mapping yellow (or Nord's aurora_yellow) to it globally, and other generators already read it, so adding it is one word in gen_conky's role tuple plus a placeholder and a palette line. No scheme file changes. Three new primitives in lib/card.lua rather than per-widget drawing: bar, ring, and a threshold helper so all four cards agree on the rule instead of each re-deriving it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: add the system widgets designDanilo M.1-0/+221
Four cards: CPU/RAM with every temperature, GPU, filesystems, and the user cache. Three were asked for; cache became its own card because it is a different refresh cycle and sharing the disks card would have made the densest card in the dashboard. Two findings that shaped it, both from probing the hardware rather than assuming: - The Arc B580 exposes no utilisation figure. The xe driver has no gpu_busy_percent, intel_gpu_top refuses the device outright, and gputop prints per-process rows with ANSI escapes, which is not an interface to build on. The card shows temperatures and fan speed and no load bar, rather than inventing a number. - /data is on a spinning disk with no hwmon chip, so there is no sda temperature to show. Reading SMART would need smartctl as root and would spin up a sleeping disk each poll. Also: the four NFS mounts are two filesystems behind two servers, so showing all four printed every number twice. One mount per server. Both new samplers follow the weather fetch's cache-and-read pattern, which exists precisely so an unreachable NFS server costs a stale cache instead of freezing the draw hook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17feat: put the weather card under the clock, scale its type to widthDanilo M.1-4/+4
Placement is column 1 row 2, directly beneath the clock, which drops to a single-row cell to make room. The card's type now scales off the cell WIDTH rather than its height. Scaling off height meant the text shrank every time the cell got shorter, and beside the clock's numerals the card read as a whisper. Width is the stable dimension for this layout and is what constrains the text anyway, since the stat rows run edge to edge. Verified against the live dashboard, not only the offscreen render: the two disagreed about type size precisely because the offscreen harness was cropped to the card and never showed it next to the clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: size the weather card to its contentDanilo M.1-2/+9
The first draft placed every band at a fixed pixel offset and let the arc absorb whatever height was left. In a 1x2 cell on this monitor that is 794px of arc: a cathedral arch under a fifth of a card of content, nothing like the mockup it was shaped after. Three changes, all visible in an offscreen render of the real widget: - Type sizes and band offsets scale with the rect, within clamps, so the proportions hold on the 1920 monitor as well as this one. - The arc gets a bounded band that follows the stats instead of being pinned to the bottom, so no dead strip opens between them. - The layout gives the widget a 1x1 cell. Its content is about 350px tall and a full-height cell cannot be filled without inventing data. Found before restarting conky by loading conky's own cairo bindings standalone and rendering the widget to a PNG, which is a far tighter loop than install, restart, switch workspace, screenshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: let the shellcheck directive bind to the source lineDanilo M.1-1/+3
`set -a; . "$ENV_FILE"; set +a` on one line meant the `# shellcheck source=/dev/null` above it attached to the `set`, not the `.`, so SC1090 fired anyway. Split across lines, the directive binds and the script lints clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: correct the .cod rationale, and stop world-reading the cacheDanilo M.2-7/+20
The comment claimed a bad key returns HTTP 200 with an error body, so the status code alone proves nothing. Measured against the live API, that is wrong: a bad key returns 401 and an unknown city 404, and with curl -f both fail at curl without the .cod check ever running. The claim came from the polybar script, which ran curl WITHOUT -f and so genuinely did receive error bodies with a success exit. The check was ported but not the reason for it. It still earns its place behind -f, for a 200 whose body is not usable weather, and the comment now says that instead of something false. The spec carried the same wrong sentence and is corrected too. Separately, the cache landed at 644 in a directory where every other generated file is 600, and the response carries the configured city and its coordinates. A umask before the write puts it at 600 without a window where it is briefly readable. The .cod discrepancy was found by the Task 6 implementer, which noticed the bad-key test produced a different message than the plan predicted and said so rather than reporting the expected text as achieved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: guard now, and clamp a negative age, in the staleness pairDanilo M.1-2/+23
is_stale and age_str were the only functions in the module doing arithmetic on an unchecked argument: a nil `now` raised, and a raise in this project is a blank dashboard with no message. Every caller passes os.time(), which cannot be nil, so this was unreachable, but it is the same bug already fixed once in icon() and the module header promises nothing raises. age_str also printed a negative age when the observation timestamp was in the future, which a clock skew can produce. "stale -17m" on the card reads as a broken widget, so the age now floors at zero and such a reading counts as fresh: weather from the future is not old. With this the whole module is raise-free, verified across every function against nil, string, table, boolean, NaN, negative and zero arguments in all three positions. Both cases were found by the Task 5 implementer, which was asked to probe beyond its given tests and reported them without changing the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: do not truncate a number in exponent formDanilo M.1-3/+18
The numeric patterns stopped at the 'e', so a temp of 1.8e1 parsed as 1.8 and the card would have drawn 2 degrees instead of 18. Wrong in the worst way available: silently, and still looking like weather. OWM has not been seen to emit exponent form, and sampling several cities near and below zero returned plain decimals throughout, so this was not reachable in practice. Fixed regardless, because two characters in a character class is cheaper than the reasoning required to be sure it stays unreachable, and unlike the NaN case noted in sun_t this one fails invisibly rather than leaving a mark. Tests now cover both a negative temperature, which is ordinary here for half the year, and the exponent form. Reverting the pattern fails them. Found by adversarial probing of the committed parser, not by the plan's own tests, which only exercise the fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: note that sun_t passes a NaN timestamp throughDanilo M.1-0/+5
NaN is a number, so the type guard admits it, and every comparison against NaN is false, so neither clamp catches it and the function returns NaN. Left unguarded on purpose. No caller can produce it: parse() matches %d+ for the sun times and `now` is os.time(). If one ever did, Cairo ignores a NaN coordinate, so the arc would lose its dot rather than the dashboard going blank. A ponytail: comment names the ceiling and where the guard would go. Found while probing the committed code, not by the tests, which do not cover NaN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: explain the Beaufort index arithmetic and why compass existsDanilo M.1-0/+9
Two comments, no behaviour change. `force - 1` reads as an off-by-one until you know the scale starts at 0 while ipairs starts at 1, so the loop index is already force + 1. Said plainly now, along with why the fall-through returns 12. compass() has no caller: the card draws arrow(). It is kept because it shares arrow()'s binning exactly, so testing both pins that shared logic from two angles, and switching the card to "S 9 km/h" becomes a one-word change. Without this note the next reader finds an unused function and has to reconstruct the reasoning. Raised by code review as the only two points worth acting on; no Critical or Important issues were found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: guard a nil timestamp in the icon lookupDanilo M.1-1/+5
icon(800, nil, 1500, 1900) compared nil against a number and raised, which in this project is a blank dashboard with no message. The module's own header promises nothing raises, and is_day() already guards the same case, so the inconsistency was the bug. Not reachable from the widget as written: it passes os.time(), and parse() refuses a response without sunrise and sunset, so no caller can currently produce the failing combination. Fixed anyway, because the contract is what future callers will rely on and the fix is one word. The test file also now says why the codepoint VALUES carry no assertion: comparing M.ICON.snow against a literal would only prove the escape was typed twice. Whether E31A draws a snow cloud is what the glyph sheet is for, and mutation testing confirms the assertions cannot see it. Found by code review. The same fix is applied to the plan, so Tasks 2-5 inherit the guarded version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17fix: correct the day/night constants in Task 1's testDanilo M.1-1/+4
DAY was 1000 against a sunrise of 1500, so every timestamp in the test evaluated as night and the first day assertion could never pass. The implementation was right; the test constants were not. DAY is now 1700, inside the 1500..1900 window, with a comment saying why the value has to be checked against the window rather than just named. Caught by the Task 1 implementer, which stopped rather than quietly inverting the day/night comparison to make the assertions pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: add the weather widget implementation planDanilo M.1-0/+1186
Nine tasks, TDD throughout: the domain tables and parser get tests, the card gets a screenshot. The icon codepoints in the plan were each rendered and inspected rather than derived. That caught U+E30A, which is present in the font and draws a sun-with-rain, standing in for snow; the correct glyph is U+E31A. A wrong-but-present codepoint fails this way, plausibly, which is why the plan tells the implementer to look at the rendered sheet instead of trusting a fontconfig query. Task 6 and Task 8 both state which outcomes are acceptable when the OWM key is still activating, so an inactive key blocks neither. Paths are relative to the repo root rather than absolute: the first version of this file hardcoded a home directory in eighteen places and the pre-commit hook rejected it, correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17docs: add the weather widget designDanilo M.1-0/+239
One vertical card merging idea1.png's two: conditions on top, a sunrise-to-sunset arc at the bottom, with the times at the arc's ends behind sunrise/sunset glyphs. Decisions worth recording rather than rediscovering: - The fetch runs from `${execi}` in conky.text, not a systemd timer, so refresh is tied to the dashboard's lifetime. conky.text is empty in this config because Cairo covers it, so whether execi fires at all was probed rather than assumed: it does, even when the command prints nothing. - Inconsolata Nerd Font already carries the Weather Icons range and the two sunrise/sunset glyphs, so no new font. Confirmed by rendering the codepoints and looking at them; an fc-list query alone had wrongly reported them missing, and a missing glyph in conky is an invisible blank rather than an error. - The cache is written atomically, since the widget reads it on an unrelated 2s cadence and would otherwise parse a half-written file. - A bad key returns HTTP 200 with a JSON error body, so the fetch checks .cod before replacing the cache, and never overwrites good data with an error. - Missing and stale states draw the normal card chrome with a notice. An empty cell was rejected: it looks exactly like a crashed widget. Also ignores .superpowers/, the brainstorming session directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16fix: draw at monitor size, not a 1200x700 minimumDanilo M.1-2/+10
Hyprland's fullscreen rule resizes the window but not conky's drawing surface, which stays at minimum_width/minimum_height. The dashboard therefore painted its backdrop over roughly the top-left third of the screen and left bare wallpaper beside it, visible as soon as the window rules put it fullscreen on special:dash. Set to the primary monitor, 2560x1080. The grid derives its cells from the surface, so the layout still fills whatever size it is given; only the surface needed to grow. Task 4's development config keeps 1200x700 deliberately: a smaller window is easier to screenshot and compare during that task, and it never runs fullscreen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: fix the toggle syntax and verify the unusual rule keysDanilo M.1-10/+36
Four defects, all found while running Task 8. The documented toggle command does not work on this machine. Under the Lua config `hyprctl dispatch` evaluates its argument as Lua, so the plain `hyprctl dispatch togglespecialworkspace dash` fails with "')' expected near 'dash'"; Hyprland even says so in its error. The working form is `hyprctl dispatch 'hl.dsp.workspace.toggle_special("dash")'`, verified toggling special:dash on and off. Also read the result from `monitors[0].specialWorkspace.name`, since activeworkspace keeps reporting the ordinary workspace underneath. `fullscreen` and `no_focus` appear in no existing rule, so they were checked against the runtime rather than left as caveats: hl.window_rule rejects an unknown field with "unknown field '<name>'", and neither produces that error, so both are accepted. A deliberate bogus key confirmed the check itself works. hl.mainMod is nil in any section other than keybindings.lua, which declares `local mainMod = "SUPER"` and never exports it. The plan hedged about this; it now states the literal to use. Task 8's class check ran `conky -c ./conky.conf`, which is gitignored generated output and absent on a fresh clone, so it now renders the config first. Its `pkill -f` also matched the running script's own command line and killed the shell's process group, returning 144 and swallowing the log it had just been told to read; `pkill -x conky` instead, fixed in Tasks 4 and 6 too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16test: make the layout assertion actually able to failDanilo M.1-2/+12
The out-of-grid check iterated `G.layout or {}`. With layout unexported that walks an empty table, so the block passed vacuously and the test could never detect the very condition it existed to check. Confirmed by unexporting layout and watching the suite still pass. Drops the fallback and asserts the export first. Verified by mutation: unexporting layout now fails with "dashboard.lua must export layout", and moving the clock to col=5 fails with "layout entry out of columns", so both paths have teeth. The plan's Step 3 expected a failure that could not happen; it now says what the failure looks like and warns that a pass there means the test is broken rather than the code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16fix: step text cursors by advance, not ink widthDanilo M.1-5/+26
The date rendered as "16 /SEP /2026", each slash jammed against the next glyph. measure() returns the ink width, which ignores leading and trailing spaces because a space carries no ink, and clock.lua was using it to advance a cursor. Measured: " / " covers 6px of ink but advances 14px, so every separator lost 8px. Splits the two uses that were conflated in one function. measure() is for centring and right-aligning, where ink width is what you want; advance() returns x_advance and is for laying out runs of text left to right. The clock's date now uses advance() and reads "16 / SEP / 2026" with even gaps, verified by screenshot. Worth the split rather than changing measure() in place: text_right is a correct caller of the ink width, so one function cannot serve both and silently returning the advance would have broken alignment instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16style: drop a duplicated comment line in the templateDanilo M.1-1/+0
An earlier patch left "c8 = 200/255 alpha." on its own line directly above the sentence that already says it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: fix the dev config gap and the removed ARGB settingsDanilo M.1-4/+20
Two defects found while running Task 4, both confirmed against the conky build rather than assumed. Task 4's development config defined color1..color7 and default_color but not default_shade_color, which palette() reads for the card fill, so following the plan verbatim drew every card in the magenta sentinel. The config now carries it, with a note saying why it is required. own_window_argb_visual has been REMOVED in conky 1.24 (ARGB is always on when available) and own_window_argb_value is deprecated; both warn on this build. Replaced with own_window_colour '#AARRGGBB', which emits no warnings. In the template that value follows the palette through a new @BODY_SHADE_RAW@ placeholder, the shade colour without its leading '#', so a scheme switch recolours the window backdrop too rather than pinning it to one hardcoded hex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16fix: read the palette from the config file, and stop leaking extentsDanilo M.1-13/+51
Two defects, both found by review of the Cairo work and both confirmed by experiment rather than reasoning. The palette mechanism could not have worked. conky_parse('${color3}') returns an empty string: conky's colour variables emit renderer escape codes into conky.text, they never evaluate to a hex string, and ${default_shade_color} is not a variable at all, so conky_parse hands the literal text back. conky.config is not exposed to Lua either (_G.conky is nil) and conky_info carries only cpu_count and update_interval. What Lua does get is conky_config, the config file's path, so the colours are now parsed out of that file, where UDT has already substituted real hex. The pattern needs [%w_]+ rather than %w+ or it misses default_color and default_shade_color. The card fill also duplicated the border colour, both being color3, so filling at 0.55 alpha and stroking the hairline at 0.9 in one hue made the border invisible and the cards read as blobs rather than the mockup's thin outlines. The fill is default_shade_color now, a role every scheme already defines. card.measure allocated a cairo_text_extents_t per call, which leaks: 5000 allocations grow Lua's heap by 182KB that GC never reclaims, and :destroy() on each does not help, measured at the same 182KB. One reused module-level struct costs 0KB. A draw hook running every 2s would have bled memory for as long as conky stayed up, which no screenshot would ever reveal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: fix a comment naming a function that does not existDanilo M.1-1/+1
The test comment said "cpu_percent", but the API is new_cpu_counter() plus :sample(). Prose only, the code was always consistent, but a comment naming a nonexistent function sends the next reader looking for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: rewrite Task 8 for the Lua Hyprland configDanilo M.1-61/+145
The live config is hyprland.lua requiring sections/, with rules declared through an hl helper API, so the planned .conf fragment with source = could not have worked. Task 8 now writes a Lua section using hl.window_rule and hl.bind, matching the existing rules. Three facts checked against the running config: - keybindings.lua already binds SUPER+D to the unnamed special workspace, so the dashboard takes SUPER+S. - autostart.lua already runs a bare conky. The dashboard replaces it, so that line changes rather than a new exec being added, which would run two instances. - The unnamed special workspace hosts the btop scratchpad, so the dashboard gets its own special:dash. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: add implementation plan for the Lua dashboardDanilo M.1-0/+1531
Ten tasks, TDD where there is logic to test. Ordered so nothing half-built touches the working desktop: pure Lua and its tests first, then pixels, then the palette and the widget, then window placement, and only last does UDT switch over to this repo's template. Leads with the platform facts verified during design, because each one invalidates the approach an implementer would otherwise reach for: conky_surface() instead of the Xlib idiom on Wayland, own_window_type normal rather than desktop, a Lua fault presenting as a blank screen, grim capturing screen coordinates so it needs the dashboard's workspace active, and Cairo having no Black font weight. The pcall error overlay is built before any widget and verified by injecting a deliberate fault, since it is the only channel that makes a later mistake visible. Fixture arithmetic in the parser tests is checked rather than asserted from memory: a wrong expected value sends the implementer chasing a bug that is not there. Paths are ~-relative throughout, so the plan does not pin to one home directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: settle weather source on OpenWeatherMapDanilo M.1-8/+49
A polybar backup script for OWM turned up, which beats the weather.com scraper: it has a working condition mapping. Port the domain knowledge, not the code. Worth keeping: the OWM condition-id ranges, day/night selection against sys.sunrise/sys.sunset, and the Beaufort wind thresholds. Becomes a table in data.lua. Not portable: polybar %{F}/%{T} format tokens, hardcoded hex (UDT roles replace them), bc conversions (Lua arithmetic), one-line output (a card). So the script shrinks to curl-and-cache and Lua does the rest. The reference script hardcodes a live API key plus city and country. The key and location move to ~/.config/udt/weather.env, outside the repo, same reasoning as roles.conf. Nothing carries a key in-repo, and weather.env is gitignored so a stray copy cannot be staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16docs: add Lua dashboard design specDanilo M.1-0/+252
Conky dashboard drawn with Cairo from Lua, on a Hyprland special workspace. Replaces UDT's variables-based conky.conf.in. Platform facts verified before fixing the design, since each one invalidates a common approach: - Cairo on Wayland needs conky_surface(). The usual cairo_xlib_surface_create idiom gets a nil drawable and visual under out_to_wayland, so every tutorial using it is wrong here. - own_window_type='normal' makes Conky a real toplevel that a Hyprland windowrule can place; 'desktop' is a layer-surface and cannot be assigned to a workspace. - Cairo output covers conky.text, so conky.text stays empty. Layout is a table of grid cells at the top of dashboard.lua, so reordering widgets is one edit and the same table works on both monitors. The 2560x1600 mockup's vertical stack does not fit the 2560x1080 primary, which is what motivates cells over pixels. hwmon glob-by-name is carried over from the old config deliberately: fixed indices drift across kernel reorders and silently read the wrong chip. v1 is one clock card end to end. Widgets are additive after that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>