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.
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.
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.
``_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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
- ``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)
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.
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.
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.
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.
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).
- 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).
- ``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.
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.
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.
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.
- 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).
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``.
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>