The fan curve previously only controlled fan index 0; secondary fans (fan 2/3) stayed on driver control. The curve can now target all fans (new default) or any individual fan(s).
Backend
hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info() returns per-fan speeds; set_fan_speed() accepts a fan index list (None = all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict); reset_fan() restores all fans to automatic.
server.py: per-GPU fan_targets state; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices, e.g. a profile from a 2-fan GPU applied to a 1-fan GPU, fall back to all fans with a warning); POST /api/fans accepts fans, POST /api/fans/speed accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets.
Persistence format is now {"curve": ..., "fans": ...}; legacy bare-curve entries in config.json migrate to "all fans" at startup.
Profiles save/apply fan_targets alongside fan_curve (old profiles load fine, defaulting to all fans).
MonitoringSample carries per-fan speeds for live gauges.
Frontend
Fans tab: All / Fan 1 / Fan 2 / … selector with live per-fan %; the selection is applied together with the curve (Discard works for either pending change).
Live monitor: per-fan gauges with sparklines for multi-fan GPUs.
Notes
On cards where fan 2 is a driver-mirrored slave (writes rejected with NOT_SUPPORTED), all-fans mode skips the locked write gracefully — fan 2 follows fan 1 via the driver anyway. Explicitly selecting a locked fan fails with a clear error.
Verified: pyright 0 errors, frontend build passes, HAL behavior tested against mocked NVML (2-fan GPU with driver-locked fan 2).
The fan curve previously only controlled fan index 0; secondary fans (fan 2/3) stayed on driver control. The curve can now target **all fans** (new default) or **any individual fan(s)**.
## Backend
- `hal/fans.py`: `get_num_fans()` via `nvmlDeviceGetNumFans`; `get_fan_info()` returns per-fan speeds; `set_fan_speed()` accepts a fan index list (`None` = all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict); `reset_fan()` restores all fans to automatic.
- `server.py`: per-GPU `fan_targets` state; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices, e.g. a profile from a 2-fan GPU applied to a 1-fan GPU, fall back to all fans with a warning); `POST /api/fans` accepts `fans`, `POST /api/fans/speed` accepts a `fan` index, `GET /api/fans` returns `num_fans`/`fans`/`fan_targets`.
- Persistence format is now `{"curve": ..., "fans": ...}`; legacy bare-curve entries in `config.json` migrate to "all fans" at startup.
- Profiles save/apply `fan_targets` alongside `fan_curve` (old profiles load fine, defaulting to all fans).
- `MonitoringSample` carries per-fan speeds for live gauges.
## Frontend
- **Fans tab**: `All` / `Fan 1` / `Fan 2` / … selector with live per-fan %; the selection is applied together with the curve (Discard works for either pending change).
- **Live monitor**: per-fan gauges with sparklines for multi-fan GPUs.
## Notes
- On cards where fan 2 is a driver-mirrored slave (writes rejected with `NOT_SUPPORTED`), all-fans mode skips the locked write gracefully — fan 2 follows fan 1 via the driver anyway. Explicitly selecting a locked fan fails with a clear error.
- Verified: pyright 0 errors, frontend build passes, HAL behavior tested against mocked NVML (2-fan GPU with driver-locked fan 2).
The fan curve previously only controlled fan index 0; secondary fans
stayed on driver control. The curve can now target all fans (new
default) or any individual fan(s).
Backend:
- hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info()
returns per-fan speeds; set_fan_speed() accepts a fan index list
(None = all fans; all-fans mode is lenient toward driver-locked
fans, explicit lists are strict); reset_fan() restores all fans.
- server.py: per-GPU fan_targets state; the poller applies the curve to
all target fans and logs write failures (once per distinct error);
activation validates targets against the hardware (stale indices fall
back to all fans); POST /api/fans accepts fans, POST /api/fans/speed
accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets.
- Persistence format is now {"curve": ..., "fans": ...}; legacy
bare-curve entries migrate to "all fans" at startup.
- Profiles save/apply fan_targets alongside fan_curve.
- MonitoringSample carries per-fan speeds for live gauges.
Frontend:
- Fans tab: All / Fan 1 / Fan 2 / ... selector with live per-fan %;
the selection is applied together with the curve.
- Live monitor: per-fan gauges with sparklines for multi-fan GPUs.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The fan curve previously only controlled fan index 0; secondary fans (fan 2/3) stayed on driver control. The curve can now target all fans (new default) or any individual fan(s).
Backend
hal/fans.py:get_num_fans()vianvmlDeviceGetNumFans;get_fan_info()returns per-fan speeds;set_fan_speed()accepts a fan index list (None= all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict);reset_fan()restores all fans to automatic.server.py: per-GPUfan_targetsstate; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices, e.g. a profile from a 2-fan GPU applied to a 1-fan GPU, fall back to all fans with a warning);POST /api/fansacceptsfans,POST /api/fans/speedaccepts afanindex,GET /api/fansreturnsnum_fans/fans/fan_targets.{"curve": ..., "fans": ...}; legacy bare-curve entries inconfig.jsonmigrate to "all fans" at startup.fan_targetsalongsidefan_curve(old profiles load fine, defaulting to all fans).MonitoringSamplecarries per-fan speeds for live gauges.Frontend
All/Fan 1/Fan 2/ … selector with live per-fan %; the selection is applied together with the curve (Discard works for either pending change).Notes
NOT_SUPPORTED), all-fans mode skips the locked write gracefully — fan 2 follows fan 1 via the driver anyway. Explicitly selecting a locked fan fails with a clear error.The fan curve previously only controlled fan index 0; secondary fans stayed on driver control. The curve can now target all fans (new default) or any individual fan(s). Backend: - hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info() returns per-fan speeds; set_fan_speed() accepts a fan index list (None = all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict); reset_fan() restores all fans. - server.py: per-GPU fan_targets state; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices fall back to all fans); POST /api/fans accepts fans, POST /api/fans/speed accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets. - Persistence format is now {"curve": ..., "fans": ...}; legacy bare-curve entries migrate to "all fans" at startup. - Profiles save/apply fan_targets alongside fan_curve. - MonitoringSample carries per-fan speeds for live gauges. Frontend: - Fans tab: All / Fan 1 / Fan 2 / ... selector with live per-fan %; the selection is applied together with the curve. - Live monitor: per-fan gauges with sparklines for multi-fan GPUs.✅ No issues found — changes look consistent with the stated intent. Ready to be merged.