Skip to content

Commit 41dc8a1

Browse files
authored
feat(vms): show inactive VMs filter + status pill cap (#97)
* feat(filters): add applyInactiveVmFilter for node-status culling * feat(vms): wire applyInactiveVmFilter into VMTable + add checkbox * feat(vms): persist showInactive toggle via ?showInactive query param * docs(vms): record show-inactive feature + bump 0.9.0 * feat(vms): cap visible status pills to 3 via DS Tabs maxVisible * docs(vms): record tab cap shipping with DS 0.14.0 * fix(vms): switch Show inactive predicate to VM-status (Decision #67) The original node-status predicate culled only ~34 VMs because most long-tail VMs have allocatedNode: null or point to a still-listed healthy node — defeating the point of the toggle. Switch to ACTIVE_VM_STATUSES (matches Overview Total VMs, Decision #65): default All count drops from ~7,900 to ~1,200, matching Reza's mental model. Filter is bypassed when a specific status pill is selected so per-status views (Unknown, Orphaned, etc.) still show their true counts.
1 parent ef68e4d commit 41dc8a1

15 files changed

Lines changed: 233 additions & 48 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ When adding a new component to `@aleph-front/ds`, follow the "Adding a New Compo
292292
- Shared filter chrome: `FilterToolbar` (optional `leading` slot, DS Tabs underline variant `size="sm"` with `overflow="collapse"` for status filters with sliding indicator and automatic overflow dropdown, `flex-1 min-w-0` on Tabs container to naturally limit visible items, optional icon-only filter toggle with active badge dot — hidden when no `onFiltersToggle` provided, `size="sm"` search input with clear) and `FilterPanel` (collapsible DS Card with reset button) used by list pages; toolbar always renders above the table+detail flex container so it never gets squeezed by the detail panel
293293
- Client-side table pagination: `usePagination` hook + `TablePagination` component (DS `Pagination`, page-size dropdown 25/50/100, "Showing X–Y of Z"), resets to page 1 on filter changes, hidden when ≤1 page. Sort is lifted into each table and runs over the full filtered dataset before pagination via `applySort` (`src/lib/sort.ts`); DS Table operates in controlled-sort mode so clicking a header re-orders all rows, not just the current page
294294
- Nodes page: sortable table with text search (hash, owner, name), status filter pills with count badges, collapsible advanced filters (Properties: Staked/IPv6/Has GPU/Confidential checkboxes; CPU Vendor: AMD/Intel multi-select; Workload: VM count range; Hardware: vCPUs/Memory ranges), 4-column glassmorphism filter panel, StatusDot indicators, vCPUs and Memory columns, CPU column (vendor + architecture via `formatCpuLabel`), GPU badge column (e.g. "2x RTX 6000 ADA"), ShieldCheck icon on confidential nodes (tooltip), sticky glass side panel with CPU section (architecture/vendor/features) and GPU section (in-use/available badges), Confidential row in panel/detail, truncated lists (6 VMs, 5 history, "+N more"), full detail view via `?view=hash` (owner, IPv6, discoveredAt, confidential computing, CPU info, GPU card with per-device status, complete history table)
295-
- VMs page: sortable table with text search (hash, node), 10 status filter pills with count badges (priority-ordered: dispatched, duplicated, misplaced, missing, orphaned, unschedulable first; scheduled, unscheduled, unknown last; default tab is All), collapsible advanced filters (VM Type: micro_vm/persistent_program/instance checkboxes with descriptions; Payment & Allocation: validated/invalidated checkboxes, allocated-to-node checkbox, requires-GPU checkbox, requires-confidential checkbox; Requirements: vCPUs/Memory ranges in GB), 3-column glassmorphism filter panel, ShieldCheck icon on confidential VMs (tooltip), sortable Last Updated column (relative time, hidden in compact mode), sticky glass side panel with allocated node name (right-aligned), GPU requirements and Confidential row in Requirements section, truncated lists (6 observed nodes, 5 history, "+N more"), full detail view via `?view=hash` (allocated node name, allocatedAt, lastObservedAt, paymentType, GPU requirements row, confidential computing row, complete history table)
295+
- VMs page: sortable table with text search (hash, node), 10 status filter pills with count badges (visible cap of 3: All / Dispatched / Scheduled — rest in the `⋯` overflow dropdown via DS Tabs `maxVisible` prop; overflow ordering: duplicated, misplaced, missing, orphaned, unschedulable, unscheduled, unknown; default tab is All), collapsible advanced filters (VM Type: micro_vm/persistent_program/instance checkboxes with descriptions; Payment & Allocation: validated/invalidated checkboxes, allocated-to-node checkbox, requires-GPU checkbox, requires-confidential checkbox, default-on "Show inactive VMs" checkbox hiding VMs whose status is not in ACTIVE_VM_STATUSES (matches Overview Total VMs definition); bypassed when a specific status pill is selected so per-status views show their true counts; `?showInactive=true` URL persistence; Requirements: vCPUs/Memory ranges in GB), 3-column glassmorphism filter panel, ShieldCheck icon on confidential VMs (tooltip), sortable Last Updated column (relative time, hidden in compact mode), sticky glass side panel with allocated node name (right-aligned), GPU requirements and Confidential row in Requirements section, truncated lists (6 observed nodes, 5 history, "+N more"), full detail view via `?view=hash` (allocated node name, allocatedAt, lastObservedAt, paymentType, GPU requirements row, confidential computing row, complete history table). All-tab count is plain when only the default-on inactive-hide is culling; switches to `filtered/total` slash format when other filters or search stack on top.
296296
- Issues page: scheduling discrepancy investigation with VMs|Nodes perspective toggle (DS Tabs pill variant `size="sm"`, `?perspective=vms|nodes`), VM perspective (table with Status/VM Hash/Issue/Scheduled On/Observed On/Last Updated, status pills All/Orphaned/Duplicated/Misplaced/Missing/Unschedulable with counts, detail panel with Schedule vs Reality card + amber issue explanation + quick facts + link to full details), Node perspective (table with Status/Node Hash/Name/Orphaned/Duplicated/Misplaced/Missing/Total VMs/Last Updated, status pills All/Has Orphaned/Has Duplicated/Has Misplaced/Has Missing, detail panel with per-discrepancy-type summary cards + discrepancy VM list), no new API calls (derived from `useIssues()` hook combining `useVMs()` + `useNodes()`), text search on both perspectives; 5 DiscrepancyStatus values: orphaned/duplicated/misplaced/missing/unschedulable
297297
- Three-tier typography: Rigid Square headings (Typekit), Titillium Web body (Typekit), Source Code Pro data (`--font-mono` override). Staggered card entrance on overview page (`card-entrance` keyframe with `--ease-spring` easing). Respects `prefers-reduced-motion`.
298298
- Network Health page (`/status`): left-aligned title with status `Badge` (success/error), glassmorphism stat cards (endpoints healthy, avg latency, last checked + recheck button), Scheduler API + Aleph API endpoint sections side-by-side (`lg:grid-cols-2`) with StatusDot/HTTP code/latency, auto-refresh every 60s, `?api=` URL override

docs/ARCHITECTURE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ Dashboard on `http://localhost:3000` lists all active previews with links. State
213213
**Key files:** `src/lib/filters.ts` (pure filter functions + types), `src/lib/filters.test.ts`, `src/hooks/use-debounce.ts`, `src/components/collapsible-section.tsx`, `src/components/filter-toolbar.tsx`, `src/components/filter-panel.tsx`, `src/components/node-table.tsx`, `src/components/vm-table.tsx`
214214
**Notes:** The visual shell (status tabs, optional filter toggle button, search input, DS Card panel chrome with reset) is shared via `FilterToolbar` and `FilterPanel` — both tables compose these with their own status config, filter content, and grid layout. `FilterToolbar` is generic over the status type and accepts an optional `leading` slot (rendered before status tabs, separated by a vertical divider) for page-specific controls like the Issues perspective toggle. The filter toggle button only renders when `onFiltersToggle` is provided — pages without advanced filters (e.g. Issues) omit it. `FilterPanel` wraps content in a DS `Card` component. Status filters use DS `Tabs` with `variant="underline"` and `overflow="collapse"` — tabs that overflow the container automatically collapse into a `⋯` dropdown. A `toTabValue()` helper maps the generic status type (which may be `undefined` for "All") to string values for Radix Tabs. Tooltips use native `title` attribute on `TabsTrigger`. Multi-select filters (VM type, payment status, CPU vendor) treat "all selected" and "none selected" identically as "no filter." Count badges show `filtered/total` format when non-status filters are active. The `VmType` values are lowercase (`"microvm"`, `"persistent_program"`, `"instance"`) matching the API wire format. Boolean checkbox filters: Staked, IPv6, Has GPU, Confidential (nodes); Allocated to a node, Requires GPU, Requires Confidential (VMs). Multi-select: CPU Vendor (AMD, Intel) on nodes. Filter panel uses a 4-column layout on nodes (`lg:grid-cols-4`: Properties, CPU Vendor, Workload, Hardware). Range slider extents (vCPUs, memory, VM count) are computed from the loaded fleet via `computeNodeFilterMaxes` / `computeVmFilterMaxes`, rounded up to the next power of two with a floor (`NODE_FILTER_MAX_FLOOR`, `VM_FILTER_MAX_FLOOR`), so the slider always covers every visible row even as the fleet's largest node grows. The same maxes are passed to `applyNodeAdvancedFilters` / `applyVmAdvancedFilters` so the "is this filter active?" check uses the dynamic extent rather than a hardcoded constant.
215215

216+
**Inactive-VM filter (default on).** The VMs page hides VMs whose `status` is not in `ACTIVE_VM_STATUSES` (`{dispatched, duplicated, misplaced, missing, unschedulable}`) by default — the same active-status definition as the Overview Total VMs card (Decision #65). State lives in `VmAdvancedFilters.showInactive` (default `undefined`/`false` = hidden); toggleable via a checkbox in the FilterPanel's Payment & Allocation column. Two-way URL persistence via `?showInactive=true` (param omitted at default). The pure filter `applyInactiveVmFilter(vms, showInactive)` runs in the pipeline only when no specific status pill is selected — clicking a non-active status pill (e.g. Unknown) bypasses the filter so per-status views always resolve to their true counts. Per-status pill count badges read directly from `filteredCounts` (untouched by the inactive filter); the All-tab badge sums only the active-status counts when `showInactive` is off. `ACTIVE_VM_STATUSES` lives in `src/lib/filters.ts` and is also imported by `src/api/client.ts` for the Overview headline. Count-badge format suppresses the `filtered/total` slash when the only thing culling rows is the default-on inactive-hide (no search, no other advanced filters) — the All-tab reads as a plain count so the default state doesn't shout.
217+
218+
**Tab visibility cap.** The DS `Tabs` component (`@aleph-front/ds@0.14.0+`) supports an optional `maxVisible?: number` prop that caps the visible tab count regardless of available width — used on the VMs page (via `FilterToolbar`'s `maxVisibleStatuses` prop) to lock the visible set to All/Dispatched/Scheduled, with the rest in the existing `` overflow dropdown. When both width-based collapse and `maxVisible` are present, the stricter limit wins. Other list pages (Nodes, Issues) omit the prop and keep pure width-based collapse, so they remain unchanged.
219+
216220
### Issues Page — Derived Data Views
217221

218222
**Context:** DevOps investigating scheduling discrepancies had no dedicated view.

docs/BACKLOG.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ Intent is agreed but there are open questions, design choices, or multi-step
5757
coordination required. Needs a brainstorm or spec before someone can execute.
5858
Multi-day / multi-PR work.
5959

60-
### 2026-05-03 - VMs page "Show inactive VMs" filter
61-
**Source:** Reza on Telegram — mirroring the "Show inactive nodes" pattern from the Aleph Account app. Currently the VMs page lists VMs across all statuses with no default culling, so VMs assigned to unreachable/removed nodes (operational noise) appear alongside actively-running ones.
62-
**Description:** Add a default-on filter that hides VMs whose `allocatedNode` resolves to a node in unreachable/removed/unknown status, with a "Show inactive VMs" checkbox to reveal them. Data is already loaded by `useVMs()` + `useNodes()`; the cross-reference is `nodes.get(vm.allocatedNode)?.status`. Open questions for brainstorm: which node statuses count as "inactive" (definitely unreachable/removed; unclear about unknown); should this also filter VMs whose own status is `unscheduled`/`scheduled` (long-tail with no active payment); does the toggle live in the main toolbar next to status pills, or in the advanced FilterPanel; should the default count badge on the All tab reflect the filtered or unfiltered total. Worth a small brainstorm + plan before implementation.
63-
**Priority:** Medium
64-
6560
### 2026-05-01 - Pre-aggregated credit totals from backend
6661
**Source:** Credit page slow-load research (Decision #60)
6762
**Description:** Ask Olivier to publish a small Aleph AGGREGATE message (or expose a precomputed endpoint) with daily/hourly credit totals + per-recipient breakdowns. Page fetches a tiny doc instead of paging through ~1440 `aleph_credit_expense` messages. Would replace the current ~20s api2 fetch with a single small request. Best long-term solution; persisted cache + prefetch + placeholder are interim wins.
@@ -194,6 +189,7 @@ Items where the path forward is clear but blocked on external work.
194189
<details>
195190
<summary>Archived items</summary>
196191

192+
- ✅ 2026-05-04 - VMs page "Show inactive VMs" filter + status pill cap — default-on filter hiding VMs whose status is not in ACTIVE_VM_STATUSES (matches Overview Total VMs definition, Decision #65); FilterPanel placement, ?showInactive=true URL persistence, bypassed when a specific status pill is selected; status pills capped to 3 visible (All / Dispatched / Scheduled) via new DS Tabs `maxVisible` prop (`@aleph-front/ds@0.14.0`), rest in `` overflow (Decision #67, Reza feedback)
197193
- ✅ 2026-05-04 - Credits recipient table: search by node name + whole-row click to `/wallet?address=…`, with `Matched: <name>` chip in Sources cell when row matched only via node name (Decision #66)
198194
- ✅ 2026-05-04 - Overview "Total VMs" semantics — count only active statuses (dispatched + duplicated + misplaced + missing + unschedulable), update subtitle (Decision #65, Reza feedback)
199195
- ✅ 2026-05-03 - Credit recipient table: drop misleading Node column, lead with Address, replace Roles with Sources column reading "2 CRNs · 1 CCN · staking" (Decision #64)

docs/DECISIONS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ Each entry includes:
1818

1919
---
2020

21+
## Decision #67 - 2026-05-04
22+
**Context:** Reza on Telegram raised that the VMs page All tab reads ~7,900 even though only ~1,200 VMs are actually doing useful work — the rest is operational long tail (`unknown`, `unscheduled`, `orphaned`, `scheduled-but-never-observed`). The "Show inactive nodes" pattern from the Aleph Account app suggested a similar toggle for VMs. Status pill bar also overcrowded: 10 statuses competing for visual real estate. First-pass implementation predicated the filter on node status (`allocatedNode` → node in `{unreachable, removed, unknown}`), per a literal reading of the brainstorm's Q1 answer; that culled only ~34 VMs because most long-tail VMs have `allocatedNode: null` or point to a still-listed healthy node. The user-visible All count barely moved, defeating the point.
23+
**Decision:** Predicate switched to VM-status: hide VMs whose `status` is not in `ACTIVE_VM_STATUSES` (`{dispatched, duplicated, misplaced, missing, unschedulable}`) — the same definition as the Overview Total VMs card (Decision #65). This shrinks the default All count from ~7,900 to ~1,200, matching the Overview headline and Reza's mental model. Toggle lives in the FilterPanel under Payment & Allocation. Two-way URL persistence via `?showInactive=true`. The filter is bypassed when a specific status pill is selected — clicking Unknown still shows all 6,307 Unknown VMs (the user explicitly asked for them). Per-status count badges read raw counts and are unaffected by the filter. The All-tab badge sums only active-status counts when `showInactive` is off. `ACTIVE_VM_STATUSES` lives in `src/lib/filters.ts` (single source of truth, imported by `client.ts`'s `getOverviewStats` so the two views can never drift). Status pills capped to 3 visible (All / Dispatched / Scheduled) via a new `maxVisible` prop on DS Tabs (`@aleph-front/ds@0.14.0`); the remaining 7 (Duplicated, Misplaced, Missing, Orphaned, Unschedulable, Unscheduled, Unknown) live in the existing `⋯` overflow dropdown.
24+
**Rationale:** Predicating on VM-status keeps the Overview headline and the VMs page consistent — "active" means the same thing in both places. The original "node-status only" rationale (conceptual crispness) collapsed under reality: VMs that are operationally dead don't all live on dead nodes; many are orphaned or unscheduled with no allocation at all. The bypass-on-pill-click rule preserves access to long-tail data without surprising-zero-count traps; the filter is a default-view convenience, not a hard scope cap. FilterPanel placement keeps the toggle quiet — the user's intent was "don't give it more attention than it needs". Plain-count fast path prevents the All-tab from screaming `N/M` on every page load. URL persistence is single-boolean two-way sync; the broader URL-persistence retrofit for other advanced filters stays parked. Tab cap at 3 (not the existing width-based collapse) is deliberate: width-based collapse expands when the detail panel closes, giving an inconsistent visible-tab count. A hard count cap is predictable: same 3 tabs regardless of viewport or selected-row state.
25+
**Alternatives considered:** Node-status-only predicate (rejected mid-session — culls ~0.4% of rows, doesn't match the user's mental model). Bundling node-status + VM-status (rejected — two reasons-for-hiding under one toggle, confusing semantics, and node-status alone produces almost no operational value). Per-status pill counts that respect the filter (rejected — clicking Unknown to see "Unknown (0)" is hostile). Toolbar placement (rejected — toolbar is for navigation/scope, not advanced filters). Inline banner on culling state (rejected — too loud for default-on). Width-only tab collapse (rejected — inconsistent with detail panel open/close). Hiding less-common statuses entirely (rejected — Unknown has 6,307 VMs, blocking access would be hostile).
26+
2127
## Decision #66 - 2026-05-04
2228
**Context:** Many node operators search the credits page by node name to find their rewards, but the recipient table's search predicate only matched `r.address` — typing a node name returned an empty result. The table's row unit is a reward address (one address can own many CRNs/CCNs), so the question was how to make node-name search work without breaking that model, and what should happen when the user clicks a row.
2329
**Decision:** (1) Extend the search predicate to also match any node name where `node.reward || node.owner === r.address`, by building an `address → [{ name, kind }]` lookup once per `nodeState` change with `useMemo`. Placeholder updated to "Search address or node name…". (2) When a row matched via node name (not address), render one `Badge fill="outline" variant="info" size="sm"` per matched node name in the Sources cell, each reading `Matched: <full-name>` — full names, no truncation, no `+N` overflow — so the user can scan rows visually and pick the right one without clicking through. (3) Make the whole row clickable via DS Table's `onRowClick`, navigating to `/wallet?address=…`. The `CopyableText` cell already calls `e.stopPropagation()` on its copy button and address link, so clicking copy still copies without triggering the row navigation. (4) Persist the search query as `?q=` in the URL via `router.replace` (no history pollution, `scroll: false`); the table reads `q` on mount via `useSearchParams`. This way the browser back button from the wallet view restores both the URL and the search state, so users don't lose their query when drilling into a row and bouncing back.

docs/plans/2026-05-04-show-inactive-vms-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
status: not-started
2+
status: done
33
date: 2026-05-04
44
spec: docs/plans/2026-05-04-show-inactive-vms-design.md
55
branch: feature/show-inactive-vms
6-
note: implementation pending — DS Tabs maxVisible prop is a prerequisite for Task 4
6+
note: All tasks shipped after DS Tabs `maxVisible` landed in @aleph-front/ds@0.14.0 (Decision #67, changelog 0.9.0).
77
---
88

99
# Show Inactive VMs Implementation Plan

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"preview": "bash scripts/preview.sh"
1515
},
1616
"dependencies": {
17-
"@aleph-front/ds": "0.13.3",
17+
"@aleph-front/ds": "0.14.0",
1818
"@phosphor-icons/react": "2.1.10",
1919
"@tanstack/query-sync-storage-persister": "5.75.5",
2020
"@tanstack/react-query": "5.75.5",

0 commit comments

Comments
 (0)