Commit Graph

7227 Commits

Author SHA1 Message Date
nicolargo
1ddd34658d Merge branch 'develop' into develop-v5 2026-05-17 11:26:07 +02:00
Nicolas Hennion
7e118d5946 Merge pull request #3557 from DeepSpace2/feat-containers-cpu-limits
feat: add cpu limit to docker, podman and lxd containers
2026-05-17 11:24:50 +02:00
Nicolas Hennion
fccf7c0401 Merge pull request #3558 from metayan/fix100
Keep auto_unit within limits, so columns stay aligned
2026-05-17 11:23:05 +02:00
Yan
b42defb1d8 Keep auto_unit within limits, so columns stay aligned
Occasionally, columns got misaligned, because auto_unit returned too
many decimals when the number was slightly below 10 or 100.
Actually, when (9.995 <= n < 10) and (99.95 < n < 100).

For example,
10*2**20-1 returned 10.00M instead of 10.0M and
100*2**20-1 returned 100.0M instead of 100M.

Tests added to verify correctness.
2026-05-16 21:45:09 +00:00
nicolargo
779dee82ab feat(v5): G4-diskio — port diskio plugin to v5 (collection)
Last entry of ``KNOWN_V5_MISSING_PLUGINS`` after this commit:
``("processlist",)``.

Model (``glances/plugins/diskio/model_v5.py``):
- Fields: disk_name (PK, string), read_count / write_count
  (rate, number, internal — exportable for IOPS consumers but
  not rendered), read_bytes / write_bytes (rate, bytespers, watched,
  ``prominent=False``, ``strict_thresholds=True``, NO default
  thresholds).
- Sustained disk traffic is host-specific (a DB server may stream
  MB/s by design) — alerts only fire when operators set
  ``read_bytes_warning=...`` per-disk or per-field in
  ``[diskio]``. ``strict_thresholds=True`` blocks the bare-``<level>``
  fallback (same pattern as memswap.sin/sout) so a legacy
  ``[diskio] careful=50`` cannot trigger spurious alerts.
- ``read_time``/``write_time`` and the derived ``read_latency`` /
  ``write_latency`` of v4 are not ported — deferred with the
  ``--diskio-latency`` mode.
- ``psutil.disk_io_counters()`` may raise or return ``None`` on
  platforms without disk I/O support — model returns ``[]`` rather
  than crashing.

Renderer (``glances/plugins/diskio/render_curses_v5.py``):

    DISK I/O              R/s     W/s
    nvme0n1               0B      0B
    sda                   1.4M   732K

- 3-cell rows, 18 + 1 + 7 + 1 + 7 = 34 chars (fits sidebar cap).
- Sorted by disk_name. Cycle-1 disks (no rate baseline) are skipped
  entirely — no ``-`` placeholder wall on startup.
- Rate cells display ``auto_unit(bytes_per_sec)`` WITHOUT a trailing
  ``/s`` — header carries the per-second semantic (v4 parity).
- Long disk names tail-truncated with leading underscore.

Adjacent:
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``("processlist",)``.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.
- v4 catalogue grows a ``## diskio`` section +  footer.

28 new tests (13 model + 15 renderer). Full v5 suite: 762 passed.
2026-05-15 17:47:07 +02:00
nicolargo
a367dce523 feat(v5): fs thresholds 70/80/90 + non-prominent + alert "ongoing" marker
Three small UX adjustments raised on the live TUI:

1. ``fs.percent`` schema: explicit ``prominent: False``. Colours the
   Used cell at alert level but no longer reverse-videos it (the
   framework defaults missing ``prominent`` to True). Consistent with
   sin/sout in memswap.

2. ``fs.percent`` default thresholds raised to 70/80/90. Filesystems
   often sit at 60-70% on healthy hosts and the old 50/70/90 ladder
   produced noisy "careful" warnings. ``mem`` keeps the stricter
   50/70/90.

3. Alert block UI clarifies active-vs-resolved state:
   - Header was ``ALERTS (N)`` — now ``ALERT (X ongoing / Y total)``.
     ``ongoing`` = unique (plugin, key, field) tuples whose
     most-recent event has a non-ok level. ``total`` = full history
     length.
   - Each event row ending in a non-ok level for the **latest** event
     of its tuple gets a visible ``(ongoing)`` suffix on the level
     cell:
       ``careful (ongoing)``
       ``careful → warning (ongoing)``
     Older non-ok events superseded by a later resolution lose the
     marker — they are no longer ongoing. Resolutions
     (``warning → ok``) never carry the marker.

7 new tests:
- ``test_percent_is_watched_but_not_prominent`` (fs schema)
- ``test_percent_default_thresholds_are_70_80_90`` (fs schema)
- 1 fs runtime test updated to the new ladder + non-prominent
- 4 renderer tests covering header counts, ongoing marker, resolution
  exemption, latest-per-tuple semantics.

Full v5 suite: 734 passed, lint clean.
2026-05-15 17:39:22 +02:00
nicolargo
d9c40b6566 fix(v5): sidebar — advance y by block.height, not block width
``_paint_block`` returns the WIDTH it painted (max row width, ≤ the
panel width). ``_paint_sidebar`` was using that return value as a
HEIGHT (``y += painted_h + 1``), so each block in the left/right
sidebar advanced the cursor by ~35 lines instead of ~5 — a huge
empty band between e.g. network and fs in the left sidebar.

``_paint_top_row`` already uses ``block.height`` directly and does
not have the bug; the fix aligns ``_paint_sidebar`` on the same
pattern. Result: one blank line between sidebar blocks, matching v4.

Regression test
``test_paint_sidebar_advances_y_by_block_height_plus_one_blank_line``
locks in the contract — two 2-row blocks painted at y=5,6 and y=8,9,
with y=7 left blank.

Full v5 suite: 729 passed (+1), lint clean.
2026-05-15 17:25:27 +02:00
nicolargo
39c5858dd6 feat(v5): G4-fs — port the fs plugin to v5 (collection)
Collection plugin keyed on ``mnt_point``. Mirrors v4
``glances/plugins/fs/__init__.py``.

Model (``glances/plugins/fs/model_v5.py``):
- Fields: mnt_point (PK), device_name, fs_type (internal), options
  (internal), size, used, free, percent.
- ``percent`` watched/prominent with the standard 50/70/90 ladder.
- ``_grab_stats`` swallows PermissionError from
  ``psutil.disk_partitions`` (locked-down hosts) and per-partition
  OSError from ``disk_usage`` (ejected media, broken NFS mount) —
  the offending partition is dropped from the cycle without aborting
  the whole update.
- SNMP support left out (architecture §10).

Renderer (``glances/plugins/fs/render_curses_v5.py``):

    FILE SYS              Used   Total
    /                   125.0G  500.0G
    /home               512.0G    1.0T

- 3-cell rows (mnt + Used + Total), 18 + 1 + 7 + 1 + 7 = 34 chars —
  fits the left-sidebar cap exactly.
- Filesystems sorted by mountpoint (v4 parity).
- ``Used`` cell inherits the percent-threshold color from
  ``_levels.<mnt>.percent``; title escalates on warning/critical.
- Long mountpoints tail-truncated with a leading underscore.
- ``--fs-free-space`` toggle and the optional ``(device)`` suffix
  deferred to a later phase pending CLI / max_width plumbing.

Adjacent (already committed by the maintainer in 1da70476):
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``(processlist, diskio)``.
- v4 catalogue grows a ``## fs`` section +  footer.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.

24 new tests (11 model + 13 renderer). Full v5 suite: 728 passed.
2026-05-15 17:18:42 +02:00
nicolargo
1da7047688 Change default thresolds minimum duration for memswap 2026-05-15 17:11:47 +02:00
nicolargo
dbcfbffd51 Change default thresolds minimum duration for memswap 2026-05-15 17:06:38 +02:00
nicolargo
0e07c22bbc Change defaulkt thresolds for memswap 2026-05-15 17:02:12 +02:00
nicolargo
cb25714f8b fix(v5): config — single-file loading, v4-aligned (no cross-file merge)
The previous loader merged keys across every available config layer
(/etc → XDG → \$GLANCES_CONFIG_FILE → -C), producing surprising
cross-file inheritance. The most visible footgun: a v4-era user XDG
glances.conf with bare ``[memswap] careful=50/warning=70/critical=90``
silently bleeding onto the v5 project conf and triggering alerts on
unrelated v5 opt-in fields (``memswap.sin``/``sout``).

Aligns with v4 ``glances/config.py::config_file_paths``: exactly **one**
config file is read.

- **``-C <path>``**: that file, no search, no fallback. A missing path
  logs a WARNING and the loader proceeds with DEFAULTS only.
- **No ``-C``**: walk the v4 search list (user → system), stop at the
  first existing entry:
    1. ``$XDG_CONFIG_HOME/glances/glances.conf`` (or
       ``~/.config/glances/glances.conf``)
    2. ``/etc/glances/glances.conf``

Drops the ``$GLANCES_CONFIG_FILE`` env-var path entirely — v5-only
sugar that did not exist in v4 and that exacerbated the merge
problem. The codebase ``DEFAULTS`` layer (intrinsic baseline) and the
``GLANCES_<SECTION>__<KEY>`` env-overlay (orthogonal to the file
question, useful for containers / CI) both stay.

Tests rewritten end-to-end (3 net new):
- ``test_xdg_does_not_inherit_etc_keys`` — regression guard for the
  bare-keys-from-/etc-leaking bug.
- ``test_cli_path_does_not_merge_with_other_files`` — ``-C`` truly
  picks one file.
- ``test_missing_cli_path_falls_back_to_defaults_only`` — non-existent
  ``-C`` path logs WARNING + uses DEFAULTS, does not silently fall
  back to the search path.
- ``test_glances_config_file_env_is_ignored`` — locked out.
- ``test_loaded_sources_contains_only_the_chosen_file`` — at most one.

Runtime verification with the real user XDG conf:
  $ -C conf/glances.conf →  loaded_sources = [conf/glances.conf]
                            [memswap] has only percent_* keys (clean).
  $ no -C                →  loaded_sources = [~/.config/glances/glances.conf]
                            (single file, no /etc merge).

Full v5 suite: 704 passed (+3), lint clean.
2026-05-15 17:00:20 +02:00
nicolargo
fa7fc0c99d fix(v5): thresholds — strict_thresholds flag isolates opt-in fields
Root cause of the "sin/sout shown in green" report: a v4-era user
XDG ``~/.config/glances/glances.conf`` carrying bare keys
``[memswap] careful=50 / warning=70 / critical=90`` gets merged with
the v5 project conf via the config layering. ``read_thresholds`` then
falls back to those bare keys for ANY watched field in the section —
including the v5-only opt-in sin/sout — so empty ``_levels`` was
never reachable in practice.

Fix: introduce a per-field schema flag ``strict_thresholds: True``
that opts the field out of the bare-``<level>`` fallback inside
``read_thresholds``. Only field-prefixed (``sin_warning``) and pk-
field-level keys still activate it.

- ``read_thresholds`` gains ``strict: bool = False`` parameter.
- ``_compute_levels_for_item`` propagates ``schema.get("strict_thresholds")``.
- ``memswap.sin`` and ``memswap.sout`` ship ``strict_thresholds: True``.
  ``memswap.percent`` keeps the legacy fallback (consistent with v4
  ``[memswap] careful=50`` applying to the swap percent).

5 new tests:
- 4 in ``test_thresholds_v5.py``: strict skips bare for scalar and
  collection cases; strict still honours field-prefixed and
  pk-field-level keys.
- 1 in ``test_plugin_memswap_v5.py``: end-to-end with a fake conf
  containing only bare keys → percent gets a _levels entry, sin/sout
  do not.

Full v5 suite: 701 passed (+5), lint clean.

Runtime check with the real user-XDG layering reproduces the fix:
``memswap _levels: {'percent': {'level': 'ok', 'prominent': True}}``
— sin/sout absent → renderer paints them in DEFAULT colour, not
green.
2026-05-15 16:52:30 +02:00
nicolargo
a715c8b363 fix(v5): memswap — sin/sout never prominent + default colour with no thresholds
Two related tweaks raised on the running TUI:

1. ``sin`` / ``sout`` must never reverse-video the cell, even when a
   user-configured threshold fires. The framework's
   ``_compute_levels_for_item`` defaults a missing ``prominent`` key to
   ``True``, so removing the flag is not enough — we need an explicit
   ``"prominent": False`` in the schema. Added on both fields.

2. When no thresholds are configured (stock install, no
   ``[memswap] sin_*`` keys), ``read_thresholds`` returns ``{}`` and
   ``_compute_levels_for_item`` skips the field via ``if not thresholds:
   continue`` — no ``_levels`` entry, ``_cell_for_field`` returns
   ``ColorRole.DEFAULT``. Behaviour was already correct but was not
   covered by a regression test; locked in via two renderer tests.

4 new tests:
- ``test_sin_sout_are_not_prominent`` — schema assertion.
- ``test_sin_threshold_from_config_carries_non_prominent_level`` —
  runtime: user-configured threshold produces a level entry with
  ``prominent=False``.
- ``test_render_sin_sout_use_default_color_without_thresholds`` —
  renderer lock: no ``_levels`` entry → DEFAULT colour, not prominent.
- ``test_render_sin_carries_level_color_when_thresholds_set`` —
  renderer with thresholds: level colour applied, still non-prominent.

Full v5 suite: 696 passed (+4), lint clean.
2026-05-15 16:40:06 +02:00
nicolargo
bbb93100c3 feat(v5): memswap — surface sin/sout in TUI, drop used/free
Two UX changes on the swap panel:

1. Body rows shifted from (total, used, free) to (total, sin, sout).
   ``used`` and ``free`` are derivable from ``percent`` + ``total`` so
   showing them on dedicated lines is redundant. Replacing them with
   the swap I/O rates surfaces actionable live paging activity:

       SWAP   25.0%
       total  16.0G
       sin    97.7K/s
       sout     0B/s

   On cycle 1 the rates are absent (no baseline yet); the renderer
   shows ``-`` for both fields and stays the same height.

2. ``sin`` / ``sout`` are watched but ship **no default thresholds**.
   Sustained swap traffic is host-specific (database servers may
   page steadily by design), so alerts only fire when the user opts
   in via ``[memswap]`` in glances.conf. Suggested ladder shipped
   commented:

       # sin_careful=40960
       # sin_warning=409600
       # sin_critical=819200
       # (and the same for sout)

   ≈ 40 / 400 / 800 KB/s. ``watched=True`` lets the framework wire
   the levels when the user uncomments them; absent defaults +
   ``read_thresholds → {} → _compute_levels_for_item skips``
   guarantees silence on a stock install.

5 new tests:
- ``test_sin_sout_are_watched_without_default_thresholds`` — schema
  asserts.
- ``test_sin_sout_no_levels_without_user_thresholds`` — runtime
  confirms no _levels entries for sin/sout when config is stock.
- ``test_sin_threshold_from_config_triggers_level`` — config-set
  ``sin_warning=10000`` actually fires a warning at 15_000 bytes/s.
- ``test_render_body_rows_have_total_sin_sout`` /
  ``test_render_does_not_show_used_or_free`` /
  ``test_render_handles_missing_sin_sout_on_first_cycle`` — renderer
  layout contract.

Full v5 suite: 692 passed (+5), lint clean.
2026-05-15 16:36:02 +02:00
Adi
b4b2118933 feat: add cpu limit to docker, podman and lxd containers 2026-05-15 17:32:25 +03:00
nicolargo
94c0c52446 chore(v5): memswap — loosen default thresholds to 60/80/95
Swap usage that would be alarming on RAM is fairly normal on a
healthy host with active paging — a fairly full swap is only really
concerning near saturation. Raise the ``[memswap] percent`` default
ladder accordingly:

- careful:  50 → 60
- warning:  70 → 80
- critical: 90 → 95

Mirrors the maintainer's hands-on threshold judgment. The
``mem`` plugin keeps the stricter 50/70/90 ladder.

Test ``test_percent_level_uses_default_thresholds`` updated to probe
the new boundary (85% → warning).
2026-05-15 16:23:52 +02:00
nicolargo
0a079d8ff5 fix(v5): alert block — close warmup→event gap + local time + date prefix
Two small UX issues raised on real-world startup observation.

1. Spurious "(no events)" between warmup and first commit.
   After warmup, the first non-ok observation enters hysteresis
   (``pending_level`` set, ``pending_since`` started). Until
   ``min_duration`` (default 5 s) elapses, no event is recorded. With
   the previous logic, ``is_initializing()`` already returned False at
   that point → the alert block briefly flashed "(no events)" before
   the first event landed.
   ``GlancesAlerts.is_initializing()`` now also returns True when any
   ``_AlertState.pending_level`` is non-None. The transition from
   "(initializing)" to actual events happens in a single step.

2. Alert timestamps shown in UTC, no date for old events.
   ``_build_event`` stores UTC ISO-8601. The renderer used to crop
   ``ts[11:19]``, displaying the raw UTC clock and losing the date
   entirely. Operators reading a UI in a non-UTC zone saw confusing
   hours; alerts older than today were undated.
   New ``_format_alert_time(ts, now=None)`` in
   ``glances/outputs/curses_renderer_v5.py``: parses the ISO string
   (treats naive timestamps as UTC for back-compat), converts via
   ``astimezone()`` to the local TZ, formats as ``HH:MM:SS`` for
   same-day events and ``MM-DD HH:MM:SS`` otherwise. Unparseable
   input falls back to the first 8 chars — never raises.
   ``render_alert_block`` gains an optional ``now`` parameter so the
   same-day cutoff is testable without TZ flakiness.

6 new tests:
- alerts: hysteresis-pending → still initializing; warmup + pending
  → fires once min_duration elapses.
- renderer: same-day formatting, other-day date prefix, naive UTC
  handling, malformed fallback, end-to-end via render_alert_block.

Full v5 suite: 687 passed (+6), lint clean.
2026-05-15 16:19:22 +02:00
nicolargo
d6b5d60641 fix(v5): alert block — distinguish warmup ("initializing") from empty
Previously the alert block always showed ``(no events)`` on an empty
history — including during the per-plugin warmup window (first 3
refresh cycles by default) where alerts physically cannot have fired
yet. Misleading: the user sees "no events" before any alert has had a
chance to materialise.

Fix:

- ``GlancesAlerts.is_initializing()`` returns ``True`` when the
  ``_plugin_cycles`` map is empty (scheduler not yet ticked) or when
  any tracked plugin still sits inside its warmup window. Switches to
  ``False`` once every ingested plugin has cleared warmup.
- ``render_alert_block(history, limit, is_initializing=False)`` gains
  the flag; on empty history it shows ``(initializing)`` during
  warmup and ``(no events)`` once truly settled. Non-empty history
  always renders the events — the flag is irrelevant then.
- ``build_frame`` accepts ``alerts_initializing`` and threads it to
  ``render_alert_block``.
- ``TuiV5._build_frame`` calls ``self.alerts.is_initializing()`` and
  passes it through.

7 new tests:
- 4 alerts (constructor → True; in-warmup → True; post-warmup → False;
  mixed plugin states → True if any is still warming);
- 3 renderer (placeholder for initializing, placeholder for empty,
  non-empty history ignores the flag).

Full v5 suite: 681 passed (+7), lint clean.
2026-05-15 16:10:51 +02:00
nicolargo
2e16cd18ee fix(v5): alerts — flag initial state events, render as bare level
When Glances starts and the first post-warmup observation is non-ok
(e.g. memory was already at careful), the alert event used to display
``ok → careful``. That is misleading: no transition actually happened
— Glances merely discovered the system was already in that state.

Fix:

- ``_AlertState`` grows ``has_committed: bool`` (default ``False``);
  it flips to ``True`` the first time the state is reconciled — either
  by matching the default ``ok`` or by committing a different observed
  level. The first transition out of the default state is therefore
  identifiable.
- ``_Transition`` grows ``is_initial: bool`` (default ``False``). All
  three commit paths in ``_reconcile`` (immediate, hysteresis-elapsed)
  derive it from ``not state.has_committed`` BEFORE flipping the flag.
- ``_build_event`` records the flag on the emitted history dict; the
  event schema gains an ``is_initial`` boolean.
- ``render_alert_block`` (curses renderer) shows the bare level
  (``"careful"``) instead of ``"ok → careful"`` when ``is_initial`` is
  ``True``. Real transitions keep the arrow.

5 new tests:

- ``test_first_event_after_warmup_is_flagged_initial`` — startup case;
- ``test_subsequent_transitions_are_not_initial`` — real changes keep
  the arrow;
- ``test_initial_flag_set_when_first_observed_is_ok_then_non_ok`` —
  first observation == ok confirms state without an event; later
  rise out of ok is a real change, not initial;
- ``test_render_alert_block_initial_state_omits_arrow`` — renderer;
- ``test_render_alert_block_transition_keeps_arrow`` — renderer.

Full v5 suite: 674 passed (+5), lint clean.
2026-05-15 14:22:38 +02:00
nicolargo
f080a48937 feat(v5): G4-memswap — port the memswap plugin to v5 (scalar)
Sister of the v5 ``mem`` plugin. Same pattern, slimmer layout
(single-column body — v4 ``memswap.msg_curse`` does not 2-col).

Model (``glances/plugins/memswap/model_v5.py``):
- ``total`` / ``used`` / ``free`` — bytes, snapshot.
- ``percent`` — watched + prominent, default thresholds 50/70/90
  (same ladder as ``mem`` for UX consistency).
- ``sin`` / ``sout`` — cumulative in v4; v5 exposes them as
  bytes/sec via ``rate: True``.
- Tolerates platforms without a swap file (Illumos, OpenBSD —
  issues #1767, #2719): psutil raises, model returns ``{}`` so
  the scheduler tick keeps going.

Renderer (``glances/plugins/memswap/render_curses_v5.py``):

    SWAP   25.0%
    total  16.0G
    used    4.0G
    free   12.0G

- Line 1: ``SWAP`` (HEADER) + percent cell coloured by ``_levels.percent``.
  Title escalates to warning/critical when the prominent percent reaches
  those levels.
- Lines 2-4: ``total`` / ``used`` / ``free`` as label/value pairs.
- Value column floored at 6 chars so it does not jiggle between cycles.

Adjacent changes:
- ``KNOWN_V5_MISSING_PLUGINS`` in ``mcp_adapter_v5`` shrinks to
  ``processlist, fs, diskio`` — memswap no longer surfaces in the
  MCP startup gap log.
- v4 catalogue (``docs/architecture/tui-v4-rendering-patterns.md``)
  grows a ``## memswap`` section +  footer pointing to the new
  renderer.

22 new tests (11 model + 11 renderer). Full v5 suite: 669 passed
(+22), lint clean.
2026-05-15 14:17:04 +02:00
nicolargo
ebe0382252 docs(v5): G3-MCP Task 5 — conf + architecture §11 (MCP endpoint)
- ``conf/glances.conf``: add a commented ``[outputs] enable_mcp``
  entry above the existing ``mcp_path`` / ``mcp_allowed_hosts`` keys.
  Notes that the gate is off by default and that ``--enable-mcp``
  flips it via the config overlay.
- ``docs/architecture/glances-v5-architecture-decisions.md``: new
  §11 "MCP endpoint" covering:
  - §11.1 opt-in lifecycle (CLI + config)
  - §11.2 adapter architecture + flow diagram
  - §11.3 resource/prompt inventory with v5 status per entry
  - §11.4 known v5 gaps (logged on mount)
  - §11.5 alert schema (v5-native, no v4 translation — decision
    logged in the G3-MCP plan)
  - §11.6 auth (HTTP middleware passes SSE through; no special MCP
    middleware needed)
  - §11.7 DNS rebinding (independent ``mcp_allowed_hosts``)
  - §11.8 out of scope (history buffer, unported v4 plugins,
    WebSocket transport)
2026-05-15 13:58:30 +02:00
nicolargo
c9e88ce1da feat(v5): G3-MCP Task 4 — surface v5↔v4 MCP gaps at startup
Operators enabling MCP via ``--enable-mcp`` must know which v4
resources won't resolve through v5 yet. Two new INFO log lines at
``attach_mcp`` mount time:

- which v4 plugins are not in the v5 registry — MCP returns
  ``ValueError("Plugin '<x>' not found")`` for those. The list is
  driven by a new ``KNOWN_V5_MISSING_PLUGINS`` tuple in
  ``glances/outputs/mcp_adapter_v5.py`` (``processlist``, ``fs``,
  ``diskio``, ``memswap`` at the time of writing).
- the deferred history semantic — ``glances://stats/<plugin>/history``
  returns ``{}`` until v5 ships a real history buffer (out of scope
  per the G3-MCP plan).

The constant is a moving target: as future v5 phases port plugins,
the entries get removed and the log naturally shrinks. The adapter
``get_plugin`` logic itself does not branch on this list — it stays
data-driven via the actual registry.

2 new tests in ``tests/test_webserver_v5.py`` verify both INFO lines
fire on a gate-on mount.

Full v5 suite: 647 passed (+2), lint clean.
2026-05-15 13:54:23 +02:00
nicolargo
c88fdebe09 feat(v5): G3-MCP Task 3 — CLI overlay + attach_mcp() called from assemble()
Wires the ``--enable-mcp`` flag (added in G2 Task 1, already validated
to require ``-s``) to the actual ``/mcp`` mount:

- ``main_v5.assemble`` propagates ``args.enable_mcp`` into
  ``config._merged["outputs"]["enable_mcp"]`` — same overlay
  mechanism used for ``args.api_doc``.
- After ``register_plugin`` populates the plugin registry,
  ``attach_mcp(app, …)`` runs. It is a no-op when the gate is off,
  so existing ``-s`` deployments are unchanged.

Tests in ``tests/test_main_v5.py``:
- ``test_assemble_server_without_enable_mcp_does_not_mount`` — ``-s``
  alone: no /mcp Mount in ``app.routes``;
- ``test_assemble_propagates_enable_mcp_overlay`` — ``-s --enable-mcp``:
  the config gate flips to True and /mcp is mounted.

Manual smoke:
- ``make run-v5-server`` → GET /mcp returns 404 (no mount).
- ``make run-v5-mcp``    → log "MCP endpoint mounted at /mcp" + GET
  /mcp returns 307 (SSE redirect, mount is reachable).

Full v5 suite: 645 passed (+2), lint clean.
2026-05-15 13:47:58 +02:00
nicolargo
488e2bee89 feat(v5): G3-MCP Task 2 — attach_mcp() mounts /mcp on the gate flag
New ``attach_mcp(app, *, config, store, plugins, alerts=None)`` in
``glances/webserver_v5.py``. Behaviour:

- Gate: ``[outputs] enable_mcp`` (default ``False``). When off, the
  function is a silent no-op — no log, no mount. The default REST
  deployment is unchanged.
- Gate on: instantiates ``McpStatsAdapter`` over the v5 store +
  plugins + alerts, hands it to the (unmodified) ``GlancesMcpServer``
  alongside an empty ``args`` namespace (the v4 class stores ``args``
  but never reads it), and mounts the SSE ASGI sub-app at ``/mcp``.
  Records the server on ``app.state.mcp_server`` for diagnostics.
- Optional dependency: when ``mcp`` is not installed
  (``MCP_AVAILABLE`` is False) the function returns ``False`` and
  logs a single WARNING pointing at ``pip install 'glances[mcp]'`` —
  never raises.
- Auth: the global v5 auth middleware passes SSE responses through
  unchanged (HTTP-level middleware, no body buffering). The MCP mount
  inherits Basic + Bearer auth from the existing ``_wire_auth`` path.
- DNS rebinding: read by ``GlancesMcpServer`` from
  ``[outputs] mcp_allowed_hosts`` (loopback only by default) — kept
  independent from ``[outputs] webui_allowed_hosts``.

6 new tests in ``tests/test_webserver_v5.py``:
- default build does not mount /mcp (404);
- ``attach_mcp`` with the gate off returns False + no mount;
- gate-on adds the Mount and stores the server on ``app.state``;
- skip path emits no log noise on the common gate-off case;
- missing ``mcp`` package → False + clear pip install WARN.

Full v5 suite: 643 passed (+6), lint clean.
2026-05-15 13:44:40 +02:00
nicolargo
cf5cfc0fbd feat(v5): G3-MCP Task 1 — McpStatsAdapter (v5→MCP duck-typed facade)
First step of the G3-MCP plan
(``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g3-mcp.md``):
bridge between v5's lockless ``StatsStoreV5`` + plugin registry +
alerts pipeline and the v4-``GlancesStats``-shape surface consumed by
``glances.outputs.glances_mcp.GlancesMcpServer``. The MCP server
class itself stays untouched.

Surface:

- ``McpStatsAdapter``
  - ``getPluginsList()`` — registry names + synthetic ``"alert"``
  - ``getAllAsDict()`` — ``StatsStoreV5.as_dict()``
  - ``getAllLimitsAsDict()`` — aggregates ``default_thresholds`` per
    plugin (synthetic plugins omitted)
  - ``get_plugin(name)`` — view or ``None``; the canonical
    "Plugin not found" error path is left to the MCP server
- ``McpPluginView``
  - ``get_raw()`` — store payload (or synthetic callable for
    ``"alert"``, which returns ``GlancesAlerts.get_history()``)
  - ``get_raw_history()`` — returns ``{}`` and logs a throttled WARN
    (once per plugin) until v5 ships real history (out of scope)
  - ``get_limits()`` — per-field ``default_thresholds`` aggregation

V5 limitations are explicit (not silent):
- No history yet → WARN-once + empty dataset.
- ``processlist`` / ``fs`` / ``diskio`` / ``memswap`` not yet ported
  → ``get_plugin`` returns ``None``; MCP raises its canonical
  ``ValueError("Plugin not found")``.
- Alert schema: v5-native (option (a) from the plan's decision log).

15 new unit tests covering plugin enumeration, raw payload, limits
aggregation, history's deferred semantic + WARN throttling, synthetic
``alert`` view, and unknown plugin → ``None`` for every v4-only name.
2026-05-15 12:10:47 +02:00
nicolargo
896eeca58f docs(v5): G3-MCP plan — port the MCP endpoint to v5 via an adapter
Plan covers 6 tasks: adapter, mount in webserver_v5 (gated by
[outputs] enable_mcp), CLI overlay propagation, explicit gap docs
(history + missing plugins), conf + architecture doc, final sweep.

Scope contract: do NOT rewrite GlancesMcpServer. Introduce a thin
McpStatsAdapter / McpPluginView in glances/outputs/mcp_adapter_v5.py
that exposes the v4-style stats interface (getPluginsList,
get_plugin(name), get_raw, get_limits, ...) over StatsStoreV5 +
plugin registry + GlancesAlerts.

Surfaces v5 gaps explicitly:
- no history → adapter returns {} + WARN log (1× per plugin);
- processlist/fs/diskio/memswap not in v5 → get_plugin returns None,
  MCP raises the canonical "Plugin not found" ValueError;
- auth+SSE: middleware must not buffer; verify or port v4
  GlancesMcpAuthMiddleware pattern.

Decision logged: alert schema = v5-native (option a, no v4 translation).
2026-05-15 12:08:07 +02:00
nicolargo
61fb09cdf9 docs(v5): G2 Task 5 — architecture §1.5 mode dispatch
- New §1.5 "Mode dispatch (CLI ↔ runtime)": alignment table, ASCII
  diagram of the assemble/serve branching, rationale (v4 mental model
  + remove unauthenticated default footgun), and open points (fate of
  --quiet/--no-tui, client mode).
- §1.4 (TUI thread): rephrased "CLI control" + "Shutdown" bullets to
  reflect that default mode no longer starts uvicorn.
- §4 (REST API): preamble notes the API server is now opt-in via -s;
  every subsection below applies to server mode exclusively.

MCP wiring is referenced as deferred to a dedicated G3-MCP plan
(consistent with Task 2 of G2 being dropped).
2026-05-15 11:26:52 +02:00
nicolargo
47c9d8241c chore(v5): G2 Task 4 — Makefile targets for the new mode dispatch
- ``run-v5``: TUI mode (the new default — help string updated to
  reflect it no longer binds a socket).
- ``run-v5-debug``: TUI mode + ``-d``.
- ``run-v5-server`` (new): REST API mode (``-s``), headless.
- ``run-v5-server-debug`` (new): REST mode + ``-d``.
- ``run-v5-mcp`` (new): REST + ``--enable-mcp``. The flag is accepted +
  validated today but the MCP mount is not yet wired into v5 — see the
  plan footer (Task 2 dropped, deferred to a dedicated G3-MCP plan).

Also updates the plan file: marks Task 2 (MCP gate) as DROPPED with a
rationale section explaining that MCP is not yet wired into v5
``webserver_v5``, so there is nothing to gate. The CLI flag + validation
shipped in Task 1 are kept; the actual mount + gate will land in
G3-MCP.
2026-05-15 11:25:39 +02:00
nicolargo
38ef93d948 feat(v5): G2 — mode-dispatched lifecycle (default=TUI only, -s=REST only)
Wires the ``-s`` / ``--server`` flag introduced in Task 1 to the actual
runtime — fulfils the G2 design alignment table:

    Mode             | Scheduler | TUI | REST  | MCP
    -----------------|-----------|-----|-------|-----
    Default          | yes       | yes | no    | no
    -s               | yes       | no  | yes   | no (Task 2)
    -s --enable-mcp  | yes       | no  | yes   | yes (Task 2)

(Plan: ``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g2.md``;
Tasks 2 and 3 were swapped — see the previous discussion thread —
because Task 1 standalone left the system in a state contradicting the
design table.)

Refactor:

- ``assemble`` returns ``app=None`` in TUI mode — no FastAPI app built,
  no plugin registration on a server. In ``-s`` mode the TUI is not
  instantiated (``-s`` is headless per alignment #1).
- ``serve`` takes ``args`` as the leading argument; branches on
  ``args.server`` inside a single ``try`` whose ``finally`` cleanly
  stops the TUI + scheduler in both modes. Server mode runs uvicorn as
  before; TUI mode awaits ``scheduler_task`` until SIGINT (raised by
  ``on_quit`` when the user types ``q``/ESC).
- ``main`` logs the mode-specific startup line ("TUI mode (no REST API
  bound)" vs the existing REST line).

Tests:

- Existing ``test_assemble_*`` cases that need a FastAPI app now pass
  ``-s`` instead of ``--no-tui``. Existing semantics preserved.
- New ``test_assemble_default_mode_builds_no_app`` and
  ``test_assemble_server_mode_skips_tui`` lock in the dispatch contract.
- ``test_assemble_server_mode_plus_no_tui_is_idempotent`` covers the
  redundant ``-s --quiet`` combo (TUI still off).
- ``test_assemble_default_mode_no_tui_disables_everything`` documents
  the scheduler-only degenerate mode (useful for test rigs).
- New ``test_serve_tui_mode_does_not_instantiate_uvicorn`` is the
  bind-no-socket regression guard: patches ``uvicorn.Server`` and
  asserts it is never called in default mode.

Manual smoke (regression guard for the bug raised by the user):
- ``python -m glances.main_v5 -C conf/glances.conf --no-tui``: no
  socket on :61208, log line confirms TUI mode.
- ``python -m glances.main_v5 -C conf/glances.conf -s``: :61208
  reachable, log line confirms REST mode.

622 v5 tests green (+5 dispatch cases), lint clean.
2026-05-15 11:20:20 +02:00
nicolargo
33a90f5eb8 feat(v5): G2 — add -s/--server and --enable-mcp CLI flags + validation
Step 1 of the mode-dispatch refactor (plan
``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g2.md``). This
commit only introduces the flags + cross-flag validation; the
dispatch wiring lands in Tasks 2 and 3.

- ``-s`` / ``--server``: opt in to the REST API mode (headless).
- ``--enable-mcp``: mount /mcp; requires ``--server`` — caught by
  ``validate_args`` with a clear stderr message + argparse exit 2.
- ``--quiet`` / ``--no-tui``: kept (open point — see plan G2 §"Open
  points"). When passed together with ``-s`` an info log notes that
  the flag is redundant.
- New ``validate_args`` helper called from ``main()`` after
  ``setup_logging``.

15 new tests in ``tests/test_cli_v5.py`` covering parser shape,
defaults, alias spellings, and the validation rules.
2026-05-15 11:07:15 +02:00
nicolargo
da396fadca docs: revert NEWS.rst — user-facing changelog, no per-phase v5 entries
Roll back the ``5.0.0a3 (Phase 2 G1)`` and ``5.0.0a2 (Phase 2 G0)``
entries added during v5 development. ``NEWS.rst`` is a user-facing
changelog updated only at v5.0 release time with breaking changes
and user-visible improvements — internal phase milestones do not
belong there. Per-phase development tracking lives in
``docs/superpowers/plans/...`` and the commit history.
2026-05-15 10:20:28 +02:00
nicolargo
f4417cdbba docs(v5): mark Phase 2 G1 per-plugin renderers in catalogue + NEWS
- Add a " v5 renderer at ..." footer line under each migrated plugin
  section of the v4 TUI rendering catalogue (cpu, mem, load, network,
  percpu). Network + percpu footers also note which v4 modes are
  deferred to G2+ (--byte / --network-cumul / --network-sum and the
  quicklook-enabled toggle).
- New NEWS.rst entry for ``5.0.0a3 (Phase 2 G1)`` summarising the
  per-plugin renderer convention, the discovery mechanism, the new
  schema renderer hints (short_name / internal), and the visual-parity
  groundwork (prominent reverse pairs, 3-cycle alert warmup, top-row
  spacing, CPU/perCPU toggle, dynamic title color, psutil baseline guard).
2026-05-15 10:18:11 +02:00
nicolargo
6ade42c142 Increase CPU context switch thresolds 2026-05-15 10:15:19 +02:00
nicolargo
35c4f20cfc fix(v5): percpu — column header names are plain text (no bold, no HEADER)
V4 parity: ``display_cpu_stats_per_line`` emits each column name via
``curse_add_line(msg)`` with no decoration. Only the leading ``CPU``
title cell carries HEADER + bold.

Regression test verifies that every column-header cell after the title
is ``ColorRole.DEFAULT`` and ``bold=False``.
2026-05-15 10:11:11 +02:00
nicolargo
81bb04bbd5 feat(v5): percpu render_curses_v5 — transposed grid (fields × cores)
Replicates v4 ``percpu.msg_curse()`` layout (Linux, quicklook disabled):

    CPU    total    user  system  iowait    idle     irq    nice   steal   guest
    CPU0   21.7%   12.5%    3.2%    0.5%   83.8%    0.0%    0.0%    0.0%    0.0%
    CPU1   11.9%    8.1%    2.0%    0.1%   89.8%    0.0%    0.0%    0.0%    0.0%
    CPU2   21.5%   15.0%    4.5%    1.2%   79.3%    0.0%    0.0%    0.0%    0.0%
    CPU3    8.1%    6.3%    1.8%    0.0%   91.9%    0.0%    0.0%    0.0%    0.0%
    CPU*   16.8%   11.2%    3.2%    0.5%   85.2%    0.0%    0.0%    0.0%    0.0%

- Fields are columns, CPU cores are rows (transposed vs the other plugins).
- OS-specific stat columns (Linux / macOS / BSD / Windows) via
  ``_os_headers()`` — mirrors v4 ``define_headers_from_os()``.
- ``CPU`` title + ``total`` column always present (G1 assumes quicklook
  disabled; TODO in module for the quicklook-enabled mode).
- ``max_cpu_display=4`` default; cores sorted by ``total`` desc, extras
  collapsed into a ``CPU*`` overflow row.
- ``CPU*`` row averages the displayed cores' stats (bug-for-bug parity
  with v4 ``summarize_all_cpus_not_displayed``).
- Per-cell coloring stays DEFAULT — v5 percpu carries no field-level
  alerts (per model_v5 docstring). The system-wide ``cpu`` plugin
  remains the source of CPU alerts.

17 new tests in tests/test_plugin_percpu_render_curses_v5.py.
2026-05-15 10:03:31 +02:00
nicolargo
eddad9a7b8 fix(v5): network — shrink name col so Tx/s isn't clipped by sidebar cap
Block width was 36 chars (name=20 + gap + rx=7 + gap + tx=7) but the
left sidebar is capped at 34 chars (`_left_sidebar_max_width=34`),
clipping the last 2 chars of the Tx/s column (`Tx/s` → `Tx`, `19.0Kb`
→ `19.0`).

Reduces `_NAME_MAX_WIDTH` from 20 to 18 so the natural row width is
exactly 34, fitting the sidebar without painter-side clipping.

Adds a regression test asserting every rendered row fits in 34 chars.
2026-05-15 09:52:16 +02:00
nicolargo
57c0f80005 feat(v5): network render_curses_v5 — Rx/s + Tx/s per-interface block
Replicates v4 ``network.msg_curse()`` default mode (rate display, bits,
two columns):

    NETWORK              Rx/s    Tx/s
    eth0                 1.1Mb  250.0Kb
    wlp0s20f3           43.9Kb   11.7Kb
    lo                      0b      0b

- Header line ``NETWORK | Rx/s | Tx/s`` with dynamic title color escalation
  from any prominent bytes_recv/bytes_sent _level (warning/critical).
- One row per interface; ``is_up=False`` and first-cycle interfaces
  (no rate yet) are filtered out (v4 issues #765 + on-the-fly creation).
- Rate values: bytes/s × 8 → bits/s, K/M/G/T scaled, ``b`` suffix; sub-K
  values stay raw (e.g. ``0b``) — matches v4 ``auto_unit + 'b'``.
- Long interface names tail-truncated with a leading underscore.
- Per-interface ``_levels`` drive rate cell color + prominent flag.

Hardcoded ``name_max_width=20`` and rate-bits-2col mode for G1. TODO
in the module documents the plumbing needed (max_width + args) to
support --byte / --network-cumul / --network-sum in G2+.

16 new tests in tests/test_plugin_network_render_curses_v5.py.
2026-05-15 09:43:50 +02:00
nicolargo
6996923192 Align prominent CPU field with Glances v4 2026-05-13 16:44:35 +02:00
nicolargo
5c649d113d test(v5): align cpu tests with new prominent flags
The cpu schema was updated so:
  total       prominent True  (kept)
  system      prominent False (kept)
  user        prominent False (kept)
  iowait      prominent True  (was False) — surface sustained I/O wait
  dpc         prominent False (kept)
  steal       prominent False (was True)  — non-prominent escalation
  ctx_switches prominent False (was True) — non-prominent escalation

Renames test_system_user_iowait_dpc_are_watched_non_prominent →
test_system_user_dpc_are_watched_non_prominent (iowait moved out).
Adds test_iowait_is_watched_prominent. Updates _levels assertions for
steal and ctx_switches to expect prominent=False. Updates the cpu
render-test fixture's _levels.steal accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:43:03 +02:00
nicolargo
8733d43d87 feat(v5): dynamic top-row spacing — first flush-left, last flush-right, evenly distributed gaps
Previously top-row blocks were separated by a fixed 3-char gap, which
left wasted right-side margin on wide terminals and crammed everything
together on narrow ones. Replace with width-aware distribution:

- First block flush-left (x=0)
- Last block flush-right (right edge at max_x - 1)
- N-1 inter-block gaps share the remaining horizontal space evenly
  (remainder pushed into leftmost gaps so distribution stays balanced
  within +/- 1 char)
- Fallback when natural content + min gaps exceeds terminal width:
  every gap collapses to _TOP_GAP_MIN (1 char) and curses clips
  overflow rather than overlapping blocks

Extracted as _top_row_gaps(widths, max_x) so the distribution is
testable in isolation. 5 new unit tests cover even distribution,
single block, empty input, narrow-terminal fallback, zero-remainder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:25:31 +02:00
nicolargo
4aa5ec5cbc Rename shortname for mem inactive 2026-05-13 16:18:52 +02:00
nicolargo
a7b9992ae8 feat(v5): mem renderer pulls short_name from schema + drop label-column floors
User feedback: column widths shouldn't reserve space for label variants
the renderer never uses. Add v4-style short_name to mem and let the
auto-size adapt:

  mem.available.short_name = 'avail'    (9 -> 5 chars)
  mem.inactive.short_name  = 'inactiv'  (8 -> 7 chars)
  mem.buffers.short_name   = 'buffer'   (7 -> 6 chars)

The mem render_curses_v5 now resolves every label via
field_label(..., prefer_short=True), so adding/changing a short_name
in the schema is enough — no renderer edit needed.

The label-column floors in cpu and mem renderers
(_CPU_LABEL_MIN_WIDTHS / _MEM_LABEL_MIN_WIDTHS) are gone too: labels
are constant strings so the content-driven auto-size is naturally
stable. Only value columns still need a fixed floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:15:24 +02:00
nicolargo
cbed074842 feat(v5): add short_name renderer hint (v4 parity)
Plugins can declare an optional short_name in fields_description for
compact label display in tight per-plugin renderers:

  ctx_switches.short_name = 'ctx_sw'
  soft_interrupts.short_name = 'sw_int'
  interrupts.short_name = 'inter'

A new field_label(schema, field_name, prefer_short=False) helper
encapsulates the resolution order:
  - prefer_short=True: short_name -> label -> field name
  - prefer_short=False (default, generic renderer): label -> field name

The cpu render_curses_v5 now pulls every column label via
field_label(..., prefer_short=True) instead of hardcoding labels.

Mirrors v4 short_name (cf. curse_add_stat in plugins/plugin/model.py).
Documented in architecture decisions section 3.2 and SKILL-plugin.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:07:10 +02:00
nicolargo
656d902585 fix(v5): guard against psutil's no-baseline first cpu_times_percent sample
psutil.cpu_times_percent(interval=0.0) requires two anchor points to
compute a delta. Before the second anchor is laid down (~1 ms after the
first), it returns either all zeros or a partial sample like
(idle=1.0, every other field=0.0). The cpu plugin computes
total = 100 - idle, so an unsettled sample produces total=99-100% — a
visible spike that persists for one TTL window (1 s by default) after
startup, while v4 stays at the real value because CpuPercent primes
psutil in __init__.

Sampler-level fix: detect unsettled samples (sum of time-percent
fields < 50%), sleep 50 ms, and re-sample once before caching. The
guard runs inside the asyncio lock so concurrent get_aggregate calls
share the same retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:00:57 +02:00
nicolargo
ff1784dbe8 feat(v5): prominent cells use explicit white-on-color pairs (v4 readability parity)
Previously, prominent cells with an alert level used the foreground
color pair + A_REVERSE. ncurses swaps fg/bg under A_REVERSE, but the
post-swap foreground comes from the terminal's default fg (often a
mid-gray on modern themes), which is hard to read on a magenta/red
background.

v4 sidesteps this by defining explicit 'white on color' pairs for the
*_LOG decoration. Same approach here: _init_colors now allocates a
second set of pairs (OK/CAREFUL/WARNING/CRITICAL each as 'white fg on
that color bg'), stored in _COLOR_PAIRS_REVERSE. _attr_for picks the
dedicated pair when a cell is prominent + alerted; falls back to
A_REVERSE on the foreground pair only when the dedicated pair can't be
allocated (terminals with limited palettes).

Tests verify: dedicated pair is used when available, fallback A_REVERSE
when not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:48:15 +02:00
nicolargo
ec74e9f570 tweak(v5): raise title-color threshold — only warning/critical escalate
Previously a prominent careful level promoted the plugin title to
CAREFUL color. Per user feedback, careful is too noisy for a title
escalation: a freshly booted machine often sits in careful for one or
two cycles. Now the title stays white (HEADER) until any prominent
field reaches warning or critical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:43:20 +02:00
nicolargo
42538a0098 feat(v5): dynamic plugin title color reflects worst prominent alert level
Plugin titles (MEM/CPU/LOAD) now adapt their color to the worst
prominent alert level in the payload:
- No prominent escalation → bold white (HEADER role, v4 TITLE parity).
- Worst prominent level is careful/warning/critical → bold + that color.

Non-prominent escalations do not promote the title color (only watched +
prominent fields count).

Implementation:
- ColorRole.HEADER curses color: cyan → white (matches v4 TITLE).
- Cell gains a 'bold' flag, decoupling the bold attribute from the
  HEADER role so alert-colored titles can still render bold.
- New helpers in curses_renderer_v5: _max_prominent_level(payload),
  title_role(payload). Handle scalar and nested (collection) _levels
  shapes.
- cpu/mem/load renderers compute the title cell color via title_role()
  and set bold=True explicitly.
- All alignment helpers (generic + cpu + mem) preserve the bold flag
  when rebuilding cells.

Tests cover: title_role priority ordering, prominent-only filtering,
collection level shape, painter A_BOLD applies on explicit bold flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:41:53 +02:00
nicolargo
a49b95acd8 fix(v5): load plugin — align corecount with load-average values
The header used '{:3}core' (7 chars) while load-average values used
'{:>6.2f}' (6 chars), so '16core' overhung the right edge by 1
character compared to '0.96'/'0.81'/'0.83'. Drop the int padding and
rjust the whole corecount cell to the same width as the value cells
(6 chars), keeping right edges aligned across all four rows of the
block.

Two new tests pin the invariant: every value cell shares the same
width, and every rendered line has the same total width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:33:08 +02:00
nicolargo
2090888370 feat(v5): load plugin renderer + v4-faithful top-row ordering
Two related changes:

1. Top row order (v4 fidelity):
   build_frame now sorts frame.top / frame.left / frame.right by their
   slot-declared position (TOP_SLOT / LEFT_SLOT / RIGHT_SLOT), not the
   discovery order. Discovery is alphabetical (cpu, load, mem); v4
   renders cpu -> mem -> load. The sort fixes the order regardless of
   discovery order.

2. load plugin per-plugin TUI renderer:
   LOAD     16core
   1 min     0.96
   5 min     0.81
   15 min    0.83

   - Line 1: LOAD HEADER + Ncore suffix from cpucore (which is marked
     internal, so the generic renderer would skip it; here we use it
     specifically as the header suffix, mirroring v4).
   - Lines 2-4: 1 min / 5 min / 15 min + value formatted as {:>6.2f}.
   - Color decoration from _levels.minN (min5 / min15 watched).
   - Irix mode (v4 --disable-irix) deferred until v5 has the flag.

Reference: glances/plugins/load/__init__.py::msg_curse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:30:14 +02:00