The hub got charts; every other machine got a snapshot. That was a storage limitation, not a UI one — per-card history simply wasn't kept for remotes. It is now, for every host, so one tab serves the whole fleet: a panel per card on a shared scale, fan speed (not collected anywhere in this project before), thermal-throttle windows shaded on the sparkline, and which card each service is sitting on. Alerts are sustained and per card, and a card pinned at a power limit you set yourself doesn't count as throttling.
Added
- The GPU tab is the same tab on every host now — with history, fan speed and per-card attribution. The hub had charts; every other machine got a still photograph. That was never a rendering choice:
host_samplespooled a box's GPUs into one row and threw GPU temperature away entirely, so a remote had nothing to chart, and the old panel's own caption admitted it. Per-card history is now stored for every host, hub included, in one table — so one renderer serves the whole fleet and the fork is deleted rather than maintained. What lands with it: fan speed, which this project has never collected anywhere (NVIDIAfan.speed, AMDpwm1/fan1_input); the deep telemetry the hub kept to itself (memory-bandwidth utilisation, core/memory clocks, power cap, memory-junction temperature, perf state, throttle reasons) now read on remotes too; and which card a service is actually sitting on, by mapping each compute process through its GPU UUID and its cgroup — so a 3×3090 box shows ollama's 63 GB split 22.5 / 22.1 / 18.8 across the cards instead of one pooled number. - One panel per card, laid out for spotting the odd one out. Same rows in the same order, and the same y-scale per metric across cards, so a taller temperature line means a hotter card rather than an artefact of auto-scaling. Thermal-throttle windows are shaded on the sparklines themselves, so when it started needs no click. A By metric view transposes the same data onto one axis when the question is "which card is the problem" rather than "what is card 1 doing". Above them, pooled KPIs each carry their own sparkline; below, all cards combined as stacked VRAM against the box total with the hottest card overlaid — maximum across cards, never the average, because an average over one card at 87 °C and two idling at 45 °C reads as a comfortable 59 °C and tells you nothing.
- Who is using the GPUs, over time and in watts. Per-service VRAM history for any host, plus an energy and cost figure per service — each card's measured draw above its idle floor, shared out in proportion to the VRAM each service holds. It is labelled an estimate everywhere it appears, because GPUs meter power per card and never per process, and the idle floor is reported as its own band rather than billed to whichever service happens to be resident.
- Alerts for cards that are throttling, overheating, or have lost a fan. Per card and per host, so one hot GPU neither hides nor implicates the others. Every threshold is paired with a duration — a card touching 85 °C for two seconds mid-batch is not an incident, and a tool that pages for it gets muted inside a week. Clearing uses hysteresis so a card hovering on the line doesn't flap. Power-capping is never an alert: a box whose cards run at a deliberately lowered power limit sits at its cap by design, and that is reported separately as Capped. Per-host threshold overrides ship with the feature for the same reason. The message names the cause — which service holds VRAM on that card, and whether the fan has any headroom left, because "fan already at 100%" is the difference between turn the fans up and you are out of cooling.
- Every host reports its AI models now — not just the hub, and not just ollama. The POSIX probe reads any recognised server answering the OpenAI-compatible
/v1/modelson its standard port (vLLM, llama.cpp, LM Studio, tabbyAPI, xinference, SGLang, LiteLLM, koboldcpp, Aphrodite, Infinity, Cortex), so a remote box running something other than ollama stops being a blank tab. Three guards keep it cheap and honest: only ports already listening are touched (from thessdata the probe collects anyway, so the usual cost is zero extra connections), a reply counts only when it parses as the documented shape — an unrelated web server on:8080is never reported as a model host — and models are listed Idle, matching how the hub treats its own non-ollama servers. The Windows probe gained the ollama read it never had (win-0.2), so Windows boxes now appear with the same size/quant/params detail as Linux ones.
Fixed
- Stacked service charts were rendering as one solid black block. Service colours were generated as
hsl(...)while several chart call sites append a two-digit alpha suffix — andhsl(210 62% 56%)ccis not a colour, so Chart.js fell back to opaque black. Colours are hex now, and the hash that picks them was scattering badly enough to give two services on the same chart near-identical purples; it spreads them properly. - The AI Models tab now answers for the host you actually clicked. Picking a remote host and opening AI Models could show "No AI models reported on host yet" while the API had that host's models all along. The panel's 15-second throttle was armed by host-switch calls that never painted anything — every host switch fires the tab's renderer whether or not you are looking at it — so the click that followed was silently skipped, and recovery was left to a poll tick running on exactly the same 15-second period. Switching hosts could also strand the previous host's card on screen under the new host's name. The panel is now cached per host (one fleet-wide fetch warms every host you might click next), stamped with the host it was drawn for, and shows "Reading host's model servers…" until an answer actually arrives — it never claims a host is empty before it has asked. The Installed models card follows the host selector too, instead of listing the whole fleet under one host's name, with an All hosts tick-box when you do want the fleet view.
- A host whose registered name differs from its hostname is no longer invisible here. Remote catalogue entries were keyed by whatever the probe's own
socket.gethostname()returned, while the dashboard filters by the name the host is registered under — so a box added as "Work" that calls itself "DESKTOP-…" never showed a single model, with no error anywhere. Entries now carry the registered name.
Upgrade: hit Update in the dashboard, or docker compose pull && docker compose up -d. Pinned image: sikamikaniko123/homelab-monitor:0.29.0.