You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -292,7 +292,7 @@ When adding a new component to `@aleph-front/ds`, follow the "Adding a New Compo
292
292
- 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
293
293
- 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
294
294
- 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.
296
296
- 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
297
297
- 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`.
298
298
- 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
**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.
215
215
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
+
216
220
### Issues Page — Derived Data Views
217
221
218
222
**Context:** DevOps investigating scheduling discrepancies had no dedicated view.
**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
-
65
60
### 2026-05-01 - Pre-aggregated credit totals from backend
66
61
**Source:** Credit page slow-load research (Decision #60)
67
62
**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.
194
189
<details>
195
190
<summary>Archived items</summary>
196
191
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)
197
193
- ✅ 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)
Copy file name to clipboardExpand all lines: docs/DECISIONS.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,6 +18,12 @@ Each entry includes:
18
18
19
19
---
20
20
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
+
21
27
## Decision #66 - 2026-05-04
22
28
**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.
23
29
**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.
0 commit comments