From c528497d9c19fdcb99660fb7fa43571996d8cffc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 15:33:44 +0000 Subject: [PATCH] TUI/WebUI v5: the data-type render modes B, 0 and T (2.X-c, part 2) Three more of 2.X-c, on both surfaces. Unlike part 1's `b`/`6`/`F`, v5 rendered only ONE mode of each of these -- but the data for the second was already collected, so each is a renderer change, not a model change. `B` disk I/O byte/s vs IOPS. `read_count`/`write_count` were already collected and flagged `internal` (out of the generic renderer's default columns, never out of the payload). They now carry v4's own `IOR/s`/`IOW/s` short names, so swapping the field pair swaps the header with it on both surfaces -- the labels come from the schema. IOPS need their own formatter: counts scale by 1000 and carry no unit, where bytes scale by 1024 and carry a `B`. `_format_count_rate` and `formatIops` are that pair, deliberately not `formatCount` (1024-based). `0` load average vs Irix percentage (issue #1554). `cpucore` was already collected and `internal`. v4 reads the count from the `core` plugin's `log_core()`; v5 reads the field the load payload already carries -- same number, no cross-plugin reach. Both surfaces keep v4's guard: an absent or zero core count falls back to the plain float rather than dividing by zero. The key changes what the cell DISPLAYS, never which level colours it -- per-core normalisation is already implicit in the thresholds (`normalize_by: cpucore`), and a test pins that. `T` network Rx/Tx apart vs combined. A divergence in route, not in value: v4 renders a `bytes_all` field its model computes; v5's schema has no such field, so both renderers sum the two rates they already carry. The sum of two rates over one interval IS the combined rate. The combined cell takes no threshold colour -- the two fields own their levels and a sum belongs to neither, which is why v4 paints its own combined cell plain. The diskio render fixture gained `read_count`/`write_count` on every row and their short names. It predated the fields, so under `B` every row was dropped for a missing value and the header fell back to the raw field names. A fixture that does not carry what the server sends tests nothing. Verified against real plugins: `B` turns `R/s 0B` into `IOR/s 0`, `0` turns `1 min 0.46` into `1 min 11.6%`, and `T` turns `642b 642b` into `1.3Kb`. 2.X-c now stands at six of nine. The remaining three are blocked on data, not on keys: `U` needs the raw counter `_transform_gauge` replaces with its rate, `L` needs `read_time`/`write_time` collected at all, and `S` needs a v5 history store. Section 10 records each with its file reference. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK --- .../glances-v5-architecture-decisions.md | 38 +++++++++++- glances/outputs/glances_curses_v5.py | 9 +++ glances/outputs/static/js/v5/PluginDiskio.vue | 31 ++++++++-- glances/outputs/static/js/v5/PluginLoad.vue | 10 +++- .../outputs/static/js/v5/PluginNetwork.vue | 27 ++++++++- glances/outputs/static/js/v5/format.js | 17 ++++++ glances/outputs/static/js/v5/hotkeys.js | 5 +- glances/outputs/static/public/glances5.js | Bin 221882 -> 223205 bytes glances/plugins/diskio/model_v5.py | 16 ++++-- glances/plugins/diskio/render_curses_v5.py | 42 +++++++++++--- glances/plugins/load/render_curses_v5.py | 46 ++++++++++++--- glances/plugins/network/render_curses_v5.py | 41 +++++++++++-- tests/fixtures/webui_render_fixtures.js | 20 +++++-- tests/js/format.test.mjs | 31 +++++++++- tests/test_curses_v5.py | 13 +++++ tests/test_plugin_diskio_render_curses_v5.py | 39 +++++++++++++ tests/test_plugin_load_render_curses_v5.py | 45 +++++++++++++++ tests/test_plugin_network_render_curses_v5.py | 48 ++++++++++++++++ tests/test_webui_v5_render.py | 54 +++++++++++++++++- 19 files changed, 486 insertions(+), 46 deletions(-) diff --git a/docs/architecture/glances-v5-architecture-decisions.md b/docs/architecture/glances-v5-architecture-decisions.md index 5d8f388b..3204e161 100644 --- a/docs/architecture/glances-v5-architecture-decisions.md +++ b/docs/architecture/glances-v5-architecture-decisions.md @@ -1236,8 +1236,42 @@ curses surface — as its own owned group. No implementation in parity wave 1 cannot avoid** — the TUI shows the path prefix only when `os.path.isdir(path)` holds (`render_curses_v5.py:329`), and a browser has no filesystem, so it shows the prefix whenever `cmdline[0]` carried one. -- **The 9 remaining data-type toggles** (`b`/`B` byte/bit, `%`, `S`, …), plus - `F` (fs free space), moved here from the show/hide line above. +- **The data-type toggles** (2.X-c, 2026-09-22). **Six of the nine shipped**, + on both surfaces, plus `F` (fs free space) which moved here from the + show/hide line above. Three are blocked, and NONE of the three is a hotkey + problem — the key is one table entry either way; what is missing is the + second render mode's DATA. + + Shipped because v5 already rendered both modes, so the key was the whole + feature: `b` (network bit/s ↔ byte/s), `6` (GPU per-card ↔ mean), `F` + (filesystem used ↔ free). + + Shipped after writing the second mode, the data being already collected: + `B` (disk I/O byte/s ↔ IOPS — `read_count`/`write_count` were collected and + `internal`, and now carry the `IOR/s`/`IOW/s` short names v4 uses), `0` + (load average ↔ Irix percentage — `cpucore` was collected and `internal`), + `T` (network Rx/Tx apart ↔ combined). + + `T` carries a deliberate divergence in route, not in value: v4 renders a + `bytes_all` field its model computes; v5's schema has no such field, so both + renderers sum the two rates they already have. The sum of two rates over one + interval is the combined rate. + + **Blocked, each needing model or infrastructure work first:** + - `U` (network live ↔ cumulative) — `_transform_gauge` + (`plugins/plugin/base_v5.py`) REPLACES a counter with its rate and keeps + the raw value only in `_raw_previous`. Exposing it is a field-contract + change to the REST payload, not a renderer change. + - `L` (disk I/O byte/s ↔ latency) — `read_time`/`write_time` are not + collected at all, explicitly deferred (`diskio/model_v5.py`). + - `S` (quicklook bar ↔ sparkline) — no v5 history store + (`quicklook/model_v5.py`). + + `F` needed a shape neither group had used: `[fs] free_space` is plugin + CONFIG and reaches the renderer as payload metadata, so the TUI's ViewState + field is tri-state (`None` = follow the payload) and the browser seeds it + from the fs payload rather than from `serverArgs`, which carries only the + CLI flag and reads `false` for a config-set `true`. - **`F5` / `Ctrl-R`** forced refresh and the sort-navigation arrow keys. The `ViewState` mechanism this group builds on already exists diff --git a/glances/outputs/glances_curses_v5.py b/glances/outputs/glances_curses_v5.py index 59bb8ef7..4ff1cb7d 100644 --- a/glances/outputs/glances_curses_v5.py +++ b/glances/outputs/glances_curses_v5.py @@ -142,6 +142,9 @@ class ViewState: # flipped by their keys. byte: bool = False meangpu: bool = False + diskio_iops: bool = False + load_irix: bool = False + network_sum: bool = False # Tri-state, unlike the two above: `[fs] free_space` lives in the fs # plugin's CONFIG and reaches the renderer as payload metadata, not as a # constructor argument the TUI could seed from. `None` therefore means @@ -236,6 +239,9 @@ class TuiV5(threading.Thread): # through the per-cycle `view` dict. "b": {"switch": "byte", "group": "TOGGLE VIEW", "desc": "Network I/O in bit/s or byte/s"}, "6": {"switch": "meangpu", "group": "TOGGLE VIEW", "desc": "GPU: per-card or mean"}, + "B": {"switch": "diskio_iops", "group": "TOGGLE VIEW", "desc": "Disk I/O in byte/s or IOPS"}, + "0": {"switch": "load_irix", "group": "TOGGLE VIEW", "desc": "Load average or Irix percentage"}, + "T": {"switch": "network_sum", "group": "TOGGLE VIEW", "desc": "Network Rx/Tx apart or combined"}, # Tri-state, so it cannot be a plain `switch`: `None` means "follow # `[fs] free_space`", which is why it has its own verb. "F": {"action": "fs_free_space", "group": "TOGGLE VIEW", "desc": "Filesystem: used or free space"}, @@ -939,6 +945,9 @@ class TuiV5(threading.Thread): view["fahrenheit"] = self._fahrenheit view["hide_public_info"] = self._hide_public_info view["byte"] = self._view.byte + view["diskio_iops"] = self._view.diskio_iops + view["load_irix"] = self._view.load_irix + view["network_sum"] = self._view.network_sum # Only published when the viewer has pressed `F`; absent means the fs # renderer keeps reading its payload metadata. if self._view.fs_free_space is not None: diff --git a/glances/outputs/static/js/v5/PluginDiskio.vue b/glances/outputs/static/js/v5/PluginDiskio.vue index 4236d17e..cf919bd1 100644 --- a/glances/outputs/static/js/v5/PluginDiskio.vue +++ b/glances/outputs/static/js/v5/PluginDiskio.vue @@ -4,7 +4,7 @@ {{ TITLE }} - + {{ labelFor(labels, field) }} @@ -16,8 +16,8 @@ {{ nameOf(item) }} - - {{ formatBytes(item[field]) }} + + {{ formatCell(item[field]) }} @@ -26,7 +26,7 @@