Skip to content

feat(console): Admin portal — Capacity (Cluster/Nodes/Storage) and Backup Classes#28

Merged
Aleksei Sviridkin (lexfrei) merged 19 commits into
mainfrom
feat/admin-portal
Jun 2, 2026
Merged

feat(console): Admin portal — Capacity (Cluster/Nodes/Storage) and Backup Classes#28
Aleksei Sviridkin (lexfrei) merged 19 commits into
mainfrom
feat/admin-portal

Conversation

@kvaps
Copy link
Copy Markdown
Member

@kvaps Andrei Kvapil (kvaps) commented Jun 1, 2026

image

Summary

Introduces a dedicated Admin portal (third top-nav entry beside Marketplace and Console) for cluster-wide operator views, and moves those views out of the tenant-facing Console. The portal is gated by access: a user sees it only if they can use at least one area (nodes/list for Capacity, backupclasses/update for Backup Classes); the nav tab is hidden otherwise and direct-URL access renders a 403.

Capacity

  • Cluster (/admin/capacity/cluster) — cluster-wide resource accounting laid out top-to-bottom: one row per resource (CPU, Memory, Storage, Pods, and each discovered extended resource such as GPUs) with Capacity / Allocatable / Requested / Used, plus circular allocation gauges (reusing the per-tenant quota rings). Each resource links to a per-resource drill-down listing the consuming Workloads (owning application, grouped from pod labels, restricted to tenant namespaces) with a deep-link to the app in Console.
  • Nodes (/admin/capacity/nodes) — the per-node breakdown, transposed so each node is a column (Status / Roles / Age in the column header, resources as rows). Resource labels link to the same per-resource drill-down; a name/role filter and a per-node min-width (horizontal scroll) keep large clusters usable.
  • Storage (/admin/capacity/storage) — PersistentVolumeClaims across tenant namespaces aggregated by StorageClass (claim count, requested, bound). Each StorageClass drills down to the consuming Workloads and their summed requested storage.

Backups

  • Backup Classes moved from Console into the Admin portal, still guarded by backupclasses/update. The per-tenant Backups screens (Plans / Backup Jobs / Backups / Restore Jobs) stay in Console.

Console

  • Cluster Usage and the admin Backup Classes entry are removed from the Console section and its sidebar.

Test plan

  • pnpm -r typecheck passes
  • pnpm --filter @cozystack/console test passes (272 tests, incl. routing/access gates, the transposed node table, both drill-downs, gauges, and the storage aggregation)
  • pnpm build:console passes
  • Verified end-to-end against a live cluster with GPUs (Admin tab gated by access; Cluster/Nodes/Storage; clicking a GPU shows the consuming tenant workload)

Note: repo-wide eslint reports pre-existing errors in files untouched by this PR; all files changed here lint clean.

Summary by CodeRabbit

  • New Features
    • Added a dedicated Admin portal with Capacity, Nodes, Storage, and Backups sections and an “Admin” top-level tab (shown only to authorized users).
    • New Cluster overview: unified aggregates table and gauge summaries for cluster-wide resources.
    • Drill-down pages: per-resource, per-storage-class, Nodes and Storage detail views plus storage-class workload lists.
  • Refactor
    • Cluster administration items moved from the Console sidebar into the Admin area; Console focuses on tenant-facing flows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d78f4941-6706-4d2c-b191-03655cacff2a

📥 Commits

Reviewing files that changed from the base of the PR and between cadafff and 5ffc781.

📒 Files selected for processing (13)
  • apps/console/src/components/cluster-usage/ClusterStorageSection.test.tsx
  • apps/console/src/components/cluster-usage/ClusterStorageSection.tsx
  • apps/console/src/components/cluster-usage/ResourceCard.test.tsx
  • apps/console/src/components/cluster-usage/ResourceCard.tsx
  • apps/console/src/hooks/useClusterUsageAccess.ts
  • apps/console/src/lib/cluster-usage/aggregate.ts
  • apps/console/src/routes/AdminPage.routing.test.tsx
  • apps/console/src/routes/AdminPage.tsx
  • apps/console/src/routes/CapacityAdminGuard.test.tsx
  • apps/console/src/routes/CapacityAdminGuard.tsx
  • apps/console/src/routes/ClusterUsageResourcePage.test.tsx
  • apps/console/src/routes/ClusterUsageResourcePage.tsx
  • apps/console/src/routes/sidebar-sections.tsx

📝 Walkthrough

Walkthrough

Adds an /admin portal with RBAC checks and a separate admin sidebar; refactors cluster usage into aggregates+gauges and a transposed nodes-as-columns table; introduces storage/resource drill-down pages, workload utilities, guards, hooks, and associated tests.

Changes

Admin Portal Infrastructure & Cluster Usage Refactor

Layer / File(s) Summary
Workload ownership resolution utility
apps/console/src/lib/workload.ts
workloadOwner derives owning application from pod labels via Cozystack lineage or Helm labels, falling back to resource name and for missing kind.
Type and quota exports
apps/console/src/components/QuotaDisplay.tsx, apps/console/src/lib/cluster-usage/types.ts
Exports QuotaEntry and GaugeCard; adds PvcSpec, PvcStatus, and Pvc types for PVC modeling.
WorkloadCell component for drill-down links
apps/console/src/components/WorkloadCell.tsx
Renders workload name with optional deep link to /console/{plural}/{name}/workloads; derives application plural from kind and tenant from namespace prefix.
Admin permission hooks and sidebar sections
apps/console/src/routes/sidebar-sections.tsx
Adds useAdminAccess, useCanSeeAdmin, and useAdminSidebarSections; Console sidebar stops inlining admin permission checks.
AdminPage route component with access gating
apps/console/src/routes/AdminPage.tsx, apps/console/src/routes/AdminPage.routing.test.tsx
AdminPage mounts at /admin/* and gates areas by permissions; routes capacity (cluster/resource/storage/nodes) and backups under guards and redirects index based on capability.
App.tsx admin routing and tab integration
apps/console/src/App.tsx
App shell computes inAdmin to choose sidebar sections and conditionally shows an Admin top tab when useCanSeeAdmin() is true; registers /admin/* route.
Capacity guard and access hook
apps/console/src/hooks/useClusterUsageAccess.ts, apps/console/src/routes/CapacityAdminGuard.tsx, tests
Adds useClusterUsageAccess (nodes/list SSAR) and CapacityAdminGuard to gate capacity routes; tests cover allowed/denied/error/pending cases.
ClusterUsageAggregates refactor: cards to table
apps/console/src/components/cluster-usage/ClusterUsageAggregates.tsx, apps/console/src/components/cluster-usage/ClusterUsageAggregates.test.tsx
Replaces ResourceCard grid with a unified table of resource rows, standard+extended rows, linkable drill-downs /admin/capacity/cluster/r/{key}, and podsUnavailable handling.
ClusterUsageGauges component
apps/console/src/components/cluster-usage/ClusterUsageGauges.tsx
New gauges component that renders quota-style cards for standard and extended resources; hides when podsUnavailable and links to drill-down when available.
ClusterUsageTable: transposed nodes-as-columns layout
apps/console/src/components/cluster-usage/ClusterUsageTable.tsx, apps/console/src/components/cluster-usage/ClusterUsageTable.test.tsx
Transposes nodes into columns, renders attribute rows (cpu/memory/extended), simplifies unavailable cell markup, and moves per-node metadata into column headers; tests updated accordingly.
ClusterUsagePage simplification
apps/console/src/routes/ClusterUsagePage.tsx, apps/console/src/routes/ClusterUsagePage.test.tsx
Page now renders only ClusterUsageAggregates (no per-node table) and updates header text to "Cluster".
Cluster storage section and summary page
apps/console/src/components/cluster-usage/ClusterStorageSection.tsx, apps/console/src/components/cluster-usage/ClusterStorageSection.test.tsx, apps/console/src/routes/StoragePage.tsx
ClusterStorageSection aggregates tenant PVCs per storage class, handles loading/error/403/empty states, and links to storage-class drill-down; StoragePage embeds it.
StorageClassUsagePage: per-storage-class workload usage
apps/console/src/routes/StorageClassUsagePage.tsx, apps/console/src/routes/StorageClassUsagePage.test.tsx
Drill-down page aggregating PVCs for a storage class by owning workload; sums requested storage and links to application pages via WorkloadCell.
ClusterUsageResourcePage: per-resource workload usage
apps/console/src/routes/ClusterUsageResourcePage.tsx, apps/console/src/routes/ClusterUsageResourcePage.test.tsx
Drill-down page showing workloads requesting a selected resource, grouping by tenant/kind/name and summing container requests only; includes CPU aggregation tests.
NodesPage: per-node cluster view
apps/console/src/routes/NodesPage.tsx, apps/console/src/routes/NodesPage.test.tsx
Admin Nodes page renders transposed node columns via ClusterUsageTable, handles loading/403/empty states, and computes extended resource keys from aggregates.
Backup class route migration to admin namespace
apps/console/src/routes/BackupClass{Create,Detail,Edit,List}Page.tsx
Backup-class pages updated to use /admin/backups/backupclasses/... routes instead of /console/....
ConsolePage route cleanup
apps/console/src/routes/ConsolePage.tsx, apps/console/src/routes/ConsolePage.routing.test.tsx
Removes cluster-usage and backup-class routes from tenant Console routes, updates imports and tests to reflect the admin split.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • cozystack/cozystack-ui#18: Implements cluster-usage admin functionality end-to-end including sidebar/admin gating, cluster-usage page, components, and data hook that directly overlap with this PR's admin-area and cluster-usage changes.

Suggested reviewers

  • lexfrei
  • androndo
  • myasnikovdaniil

Poem

🐰 A rabbit hops through portals new,
Admin gates swing soft and true,
Cards condense to rows that shine,
Storage, nodes, and resource lines —
Drill-downs bloom where data grew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: introducing an Admin portal with Capacity (Cluster/Nodes/Storage) and Backup Classes features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-portal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files area/console Issues or PRs related to apps/console — routes, detail pages, marketplace, command palette kind/feature Categorizes issue or PR as related to a new feature labels Jun 1, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a dedicated Admin portal (/admin/*) gated by operator permissions, moving cluster-wide views like Cluster Usage and Backups out of the tenant-facing Console section. It also refactors the Cluster Usage aggregate view into a unified table and adds a per-resource drill-down page to view consumers grouped by tenant namespace. Feedback on these changes includes filtering out pods in Succeeded or Failed phases to avoid over-reporting resources, removing the 100% cap on resource request percentages to expose overcommit metrics, and factoring in initContainers when calculating a pod's resource requests to prevent under-reporting.

Comment on lines +81 to +83
for (const pod of podsList?.items ?? []) {
const requested = podResourceRequest(pod, resource)
if (requested <= 0) continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Pods in the Succeeded or Failed phase (such as completed Jobs or finished single-run pods) no longer consume node resources. Including them in the calculation will significantly over-report the requested resources in the cluster. Consider filtering out pods that are not active.

Suggested change
for (const pod of podsList?.items ?? []) {
const requested = podResourceRequest(pod, resource)
if (requested <= 0) continue
for (const pod of podsList?.items ?? []) {
const phase = pod.status?.phase
if (phase === "Succeeded" || phase === "Failed") continue
const requested = podResourceRequest(pod, resource)
if (requested <= 0) continue

Comment on lines +56 to +59
function percent(value: number, allocatable: number): number | null {
if (allocatable <= 0) return null
return Math.min(100, Math.round((value / allocatable) * 100))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Capping the percentage at 100% hides resource overcommit, which is a critical metric for cluster administrators (e.g., knowing if CPU or Memory requests exceed 100% of allocatable capacity). Consider removing Math.min(100, ...) to allow percentages over 100% to be displayed.

Suggested change
function percent(value: number, allocatable: number): number | null {
if (allocatable <= 0) return null
return Math.min(100, Math.round((value / allocatable) * 100))
}
function percent(value: number, allocatable: number): number | null {
if (allocatable <= 0) return null
return Math.round((value / allocatable) * 100)
}

Comment on lines +43 to +52
function podResourceRequest(pod: Pod, resource: string): number {
let total = 0
for (const container of pod.spec?.containers ?? []) {
const req = container.resources?.requests?.[resource]
const lim = container.resources?.limits?.[resource]
const value = req ?? lim
if (value !== undefined) total += parseQuantity(value)
}
return total
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation only sums requests for app containers (pod.spec.containers), completely ignoring initContainers. In Kubernetes, the effective request of a pod is the maximum of the sum of app containers' requests and the maximum of any init container's request. Ignoring init containers can lead to under-reporting resource requests.

function podResourceRequest(pod: Pod, resource: string): number {
  let containersTotal = 0
  for (const container of pod.spec?.containers ?? []) {
    const req = container.resources?.requests?.[resource]
    const lim = container.resources?.limits?.[resource]
    const value = req ?? lim
    if (value !== undefined) containersTotal += parseQuantity(value)
  }
  let initContainersMax = 0
  for (const container of pod.spec?.initContainers ?? []) {
    const req = container.resources?.requests?.[resource]
    const lim = container.resources?.limits?.[resource]
    const value = req ?? lim
    if (value !== undefined) {
      initContainersMax = Math.max(initContainersMax, parseQuantity(value))
    }
  }
  return Math.max(containersTotal, initContainersMax)
}

Andrei Kvapil (kvaps) and others added 11 commits June 1, 2026 21:23
Lay the cluster-usage resources out top-to-bottom (one row per resource:
CPU, Memory, Storage, Pods, then each discovered extended resource)
instead of a left-to-right card grid. Each requestable resource row links
to a new drill-down page that lists who consumes it across the cluster,
grouped by tenant namespace and owning application (read from pod
apps.cozystack.io/application.{kind,name} labels). The resource key flows
through a splat route so vendor.com/model GPU names survive routing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…in portal

Add an Admin top-nav entry (Marketplace / Console / Admin) with its own
sidebar and relocate the cluster-wide operator views out of the
tenant-facing Console: Cluster Usage and the Backup Classes management
added in cozystack-ui#21. Per-tenant backups (Plans / Backup Jobs /
Backups / Restore Jobs) stay in Console.

The Admin tab and /admin/* routes are gated by useAdminAccess — a user
reaches the portal if they can use at least one area (nodes/list for
Cluster Usage or backupclasses/update for Backup Classes); the tab is
hidden otherwise and direct-URL access renders a 403. Each area keeps its
own guard (the Cluster Usage page on nodes/list, the Backup Classes routes
via BackupClassAdminGuard). Backup Classes internal links are repointed to
/admin/backups/backupclasses.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…tion

Each consumer row in the per-resource drill-down now deep-links to the
deployed application's Console page (/console/<plural>/<name>), resolving
the plural from the application kind via ApplicationDefinitions and
switching the Console tenant context to the consumer's namespace on click.
Only real app instances are linked — the row must live in a tenant
namespace and its kind must resolve to a known application; everything
else (system pods, unrecognised owners) stays plain text.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…ed table

Rename the Admin sidebar section Cluster → Resources and add a dedicated
Nodes entry beside Cluster Usage. The per-node breakdown moves off the
Cluster Usage page (now just the cluster-wide resources table) onto its own
/admin/nodes page, and the node table is transposed: each node is a column
and each attribute (Status, Roles, CPU, Memory, every extended resource,
Age) is a row read top-to-bottom, with a sticky label column and a
name/role filter that narrows the visible node columns.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…rill-down to per-node usage

Rename the admin Cluster Usage entry/page to Resources and move the routes
to /admin/resources-usage and /admin/resources-nodes. Clicking a resource
now opens a per-node usage page (same Capacity / Allocatable / Requested /
Used columns as the Resources table, pivoted to one row per node that
exposes the resource) instead of the consumer-by-tenant breakdown.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…view

Clicking a resource on the Resources page again opens the consumer
breakdown (which tenant namespace and owning application uses it, derived
from pod labels, with a deep-link to the deployed application) rather than
the per-node usage table. Keeps the Resources/resources-usage renames; the
back-link points at /admin/resources-usage.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
…sumers, usage gauges

- Rename the admin section to Capacity with Cluster (/admin/capacity/cluster)
  and Nodes (/admin/capacity/nodes) entries; drill-down at
  /admin/capacity/cluster/r/*.
- Make resource row labels in the transposed Nodes table link to the same
  per-resource consumer drill-down as the Cluster resources table.
- Restrict the resource consumer drill-down to tenant namespaces (skip
  system/control-plane namespaces).
- Add cluster-wide allocation gauges (Requested vs Allocatable rings) atop the
  Cluster page, reusing the per-tenant quota GaugeCard for a consistent look.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
- Move Status / Roles / Age into each node's column header in the transposed
  Nodes table, so the body rows are purely resources (CPU, Memory, extended).
- Make the cluster usage gauges clickable: each ring links to the per-resource
  consumer drill-down (Pods excluded, matching the resources table).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Collapse the Kind / Name / Pods columns in the per-resource drill-down into a
single Workload column (owning application name with its kind as a subtitle,
deep-linked to the Console app page), dropping the raw pod count. Consumers
are grouped by the owning application (apps.cozystack.io/application labels),
which the lineage controller also stamps on the workload's PVCs and Services.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
- Add a Persistent Storage panel to the Cluster page: tenant-namespace PVCs
  aggregated by StorageClass (claim count, requested and bound totals). Each
  StorageClass links to a per-class drill-down listing the consuming workloads
  (owning application) and their summed requested storage.
- Extract a shared WorkloadCell component and workloadOwner helper so both the
  node-resource and storage drill-downs render the workload deep-link
  identically; refactor the resource drill-down to use them.
- Give the transposed Nodes table a per-node min-width so it scrolls
  horizontally instead of squishing when there are many nodes.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Add a Storage entry to the Capacity section (/admin/capacity/storage) and
move the PersistentVolumeClaim-by-StorageClass panel off the Cluster page onto
its own page, with loading and empty states. The per-StorageClass drill-down is
unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
@kvaps Andrei Kvapil (kvaps) changed the title feat(console): admin portal for Cluster Usage and Backups with per-resource drill-down feat(console): Admin portal — Capacity (Cluster/Nodes/Storage) and Backup Classes Jun 1, 2026
@kvaps Andrei Kvapil (kvaps) marked this pull request as ready for review June 1, 2026 22:14
Worker-node VMs of a tenant Kubernetes cluster (and their virt-launcher
pods) are created by Cluster API, not the cozystack lineage controller,
so they carry only CAPI labels and lack the
apps.cozystack.io/application.{kind,name} labels that workloadOwner keys
on. As a result they showed as plain text in the resource-consumer
Workload column instead of linking to the owning app.

Map pods carrying cluster.x-k8s.io/cluster-name=kubernetes-<instance>
back to the kubernetes app instance <instance>, so worker VMs of the
same cluster group together and deep-link to its Console page, matching
how a standalone VMInstance is linked today.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/console/src/routes/AdminPage.routing.test.tsx (1)

43-69: ⚡ Quick win

Add a backupclasses-only direct-capacity regression case.

The suite never exercises { nodes: false, backupclasses: true } hitting /capacity/cluster, which is the case that validates the per-area route split. Adding that case here would keep this auth boundary from regressing silently.

Proposed test
 describe("AdminPage routing & access gate", () => {
   it("renders the Cluster Usage page at /cluster-usage for an operator", async () => {
     renderWithK8sProvider(<AdminPage />, {
       client: makeClient({ nodes: true }),
       initialRoute: "/capacity/cluster",
     })
     expect(await screen.findByText("Cluster")).toBeInTheDocument()
   })
@@
   it("blocks direct access with a 403 notice when the user has neither admin area", async () => {
     renderWithK8sProvider(<AdminPage />, {
       client: makeClient({ nodes: false, backupclasses: false }),
       initialRoute: "/capacity/cluster",
     })
     expect(
       await screen.findByText(/you do not have permission to access the admin portal/i),
     ).toBeInTheDocument()
   })
+
+  it("blocks direct capacity access when only backup class admin is allowed", async () => {
+    renderWithK8sProvider(<AdminPage />, {
+      client: makeClient({ nodes: false, backupclasses: true }),
+      initialRoute: "/capacity/cluster",
+    })
+    expect(
+      await screen.findByText(/you do not have permission/i),
+    ).toBeInTheDocument()
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/routes/AdminPage.routing.test.tsx` around lines 43 - 69, Add
a new test in AdminPage.routing.test.tsx alongside the existing cases to cover
the { nodes: false, backupclasses: true } scenario: use
renderWithK8sProvider(<AdminPage />) with client: makeClient({ nodes: false,
backupclasses: true }) and initialRoute "/capacity/cluster", then assert that
the page either renders the Cluster content (if backupclasses grants access) or
shows the proper 403 notice (matching the existing permission message) depending
on intended behavior; place this test near the other routing tests so the
per-area route split for AdminPage is exercised and protected from regressions.
apps/console/src/routes/ClusterUsageResourcePage.tsx (1)

28-34: 💤 Low value

UsageRow.pods is computed but never displayed.

pods is tracked in the aggregation (incremented at Lines 80 and 83) yet the table only renders namespace, workload, and requested. Either surface it as a column or drop the field to avoid dead state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/routes/ClusterUsageResourcePage.tsx` around lines 28 - 34,
The UsageRow type includes a pods field that is incremented during aggregation
(see the code that updates UsageRow.pods) but never rendered; either remove pods
from the UsageRow type and stop incrementing it, or add a "Pods" column to the
table output that displays row.pods. To fix, in ClusterUsageResourcePage.tsx
locate the UsageRow definition and the table/rows rendering (where namespace,
workload/kind/name and requested are rendered) and either (A) delete the pods
property and remove the increments, or (B) add a new column header "Pods" and
render the pods value for each UsageRow (e.g., include row.pods in the cell
renderer alongside the other fields) so the tracked value is surfaced. Ensure
consistency between the aggregation logic (where pods is incremented) and the
table columns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/console/src/components/cluster-usage/ClusterUsageTable.tsx`:
- Around line 138-148: Update the top doc comment in ClusterUsageTable.tsx to
reflect the refactor that moved Status, Roles, and Age into each node's column
header (see the code that renders node headers around where Status/Roles/Age are
created) so it no longer says those are rows; explicitly state that only CPU,
Memory, and discovered extended-resource keys are rendered as attributeRows and
that Status/Roles/Age appear in the per-node header columns and the first column
remains a sticky label column used for attribute row labels.

In `@apps/console/src/routes/AdminPage.tsx`:
- Around line 54-67: The capacity routes are still registered even when
canClusterUsage is false, allowing unauthorized access; modify the route
registration in AdminPage to guard the entire capacity subtree by either
wrapping the capacity Route elements with a guard component (e.g., render
<BackupClassAdminGuard> around the Routes that include ClusterUsagePage,
ClusterUsageResourcePage, StorageClassUsagePage, StoragePage, NodesPage) or
conditionally register those Route entries only when canClusterUsage is true,
ensuring the Navigate default remains unchanged but the capacity/* routes are
not mounted unless canClusterUsage is true.

In `@apps/console/src/routes/ClusterUsageResourcePage.tsx`:
- Around line 69-86: In ClusterUsageResourcePage.tsx, when iterating podsList
items, skip terminal and unscheduled/orphaned pods by checking pod.status.phase
and pod.spec.nodeName (or other indicator used in aggregate.ts) before computing
requested — specifically ignore pods where pod.status.phase === "Succeeded" ||
pod.status.phase === "Failed" and those considered unscheduled/orphaned so the
drill-down matches aggregate.ts; keep using podResourceRequest,
TENANT_NAMESPACE_PREFIX, workloadOwner, byKey, and totalRequested. Also wire the
accumulated UsageRow.pods into the rendered table (add a "Pods" column or render
the pods count where UsageRow is displayed) so the pods counter that is
incremented is actually shown.

---

Nitpick comments:
In `@apps/console/src/routes/AdminPage.routing.test.tsx`:
- Around line 43-69: Add a new test in AdminPage.routing.test.tsx alongside the
existing cases to cover the { nodes: false, backupclasses: true } scenario: use
renderWithK8sProvider(<AdminPage />) with client: makeClient({ nodes: false,
backupclasses: true }) and initialRoute "/capacity/cluster", then assert that
the page either renders the Cluster content (if backupclasses grants access) or
shows the proper 403 notice (matching the existing permission message) depending
on intended behavior; place this test near the other routing tests so the
per-area route split for AdminPage is exercised and protected from regressions.

In `@apps/console/src/routes/ClusterUsageResourcePage.tsx`:
- Around line 28-34: The UsageRow type includes a pods field that is incremented
during aggregation (see the code that updates UsageRow.pods) but never rendered;
either remove pods from the UsageRow type and stop incrementing it, or add a
"Pods" column to the table output that displays row.pods. To fix, in
ClusterUsageResourcePage.tsx locate the UsageRow definition and the table/rows
rendering (where namespace, workload/kind/name and requested are rendered) and
either (A) delete the pods property and remove the increments, or (B) add a new
column header "Pods" and render the pods value for each UsageRow (e.g., include
row.pods in the cell renderer alongside the other fields) so the tracked value
is surfaced. Ensure consistency between the aggregation logic (where pods is
incremented) and the table columns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 137a756f-ec76-4be4-8b4e-67d67b57b277

📥 Commits

Reviewing files that changed from the base of the PR and between 27e9b1b and d3fd9ac.

📒 Files selected for processing (32)
  • apps/console/src/App.tsx
  • apps/console/src/components/QuotaDisplay.tsx
  • apps/console/src/components/WorkloadCell.tsx
  • apps/console/src/components/cluster-usage/ClusterStorageSection.test.tsx
  • apps/console/src/components/cluster-usage/ClusterStorageSection.tsx
  • apps/console/src/components/cluster-usage/ClusterUsageAggregates.test.tsx
  • apps/console/src/components/cluster-usage/ClusterUsageAggregates.tsx
  • apps/console/src/components/cluster-usage/ClusterUsageGauges.tsx
  • apps/console/src/components/cluster-usage/ClusterUsageTable.test.tsx
  • apps/console/src/components/cluster-usage/ClusterUsageTable.tsx
  • apps/console/src/lib/cluster-usage/types.ts
  • apps/console/src/lib/workload.test.ts
  • apps/console/src/lib/workload.ts
  • apps/console/src/routes/AdminPage.routing.test.tsx
  • apps/console/src/routes/AdminPage.tsx
  • apps/console/src/routes/BackupClassCreatePage.tsx
  • apps/console/src/routes/BackupClassDetailPage.tsx
  • apps/console/src/routes/BackupClassEditPage.tsx
  • apps/console/src/routes/BackupClassListPage.tsx
  • apps/console/src/routes/ClusterUsagePage.test.tsx
  • apps/console/src/routes/ClusterUsagePage.tsx
  • apps/console/src/routes/ClusterUsageResourcePage.test.tsx
  • apps/console/src/routes/ClusterUsageResourcePage.tsx
  • apps/console/src/routes/ConsolePage.routing.test.tsx
  • apps/console/src/routes/ConsolePage.tsx
  • apps/console/src/routes/NodesPage.test.tsx
  • apps/console/src/routes/NodesPage.tsx
  • apps/console/src/routes/StorageClassUsagePage.test.tsx
  • apps/console/src/routes/StorageClassUsagePage.tsx
  • apps/console/src/routes/StoragePage.tsx
  • apps/console/src/routes/sidebar-sections.test.tsx
  • apps/console/src/routes/sidebar-sections.tsx
💤 Files with no reviewable changes (1)
  • apps/console/src/routes/ConsolePage.tsx

Comment on lines 138 to 148
/**
* Per-node table rendered below the aggregate panel. Fixed columns
* (Name, Status, Roles, CPU, Memory) plus one column per full
* extended-resource key found in the cluster, then Age. Headers click
* to sort; default sort is Name ascending. A filter input above the
* table filters by name and roles substring.
* Per-node table, transposed: each NODE is a column and each attribute
* (Status, Roles, CPU, Memory, every discovered extended-resource key, then
* Age) is a row, read top-to-bottom. The first column is a sticky label
* column; node columns scroll horizontally when they overflow. The filter
* input narrows which node columns are shown (by name or role).
*
* NotReady nodes show em dashes for CPU / Memory because status.capacity
* stops being authoritative; the rest of the row remains visible so the
* row remains a useful pointer for the operator. When pods-list failed
* cluster-wide, Requested values in every cell are replaced by an em
* dash with a tooltip explaining the missing permission.
* stops being authoritative. When the cluster-wide pods list failed,
* Requested figures are replaced by an em dash with an explanatory tooltip.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale doc comment: Status/Roles/Age are headers, not rows.

The comment states these render as rows top-to-bottom, but the refactor moves Status, Roles, and Age into each node's column header (lines 217-219); only CPU/Memory/extended remain as attributeRows.

📝 Suggested wording fix
 /**
  * Per-node table, transposed: each NODE is a column and each attribute
- * (Status, Roles, CPU, Memory, every discovered extended-resource key, then
- * Age) is a row, read top-to-bottom. The first column is a sticky label
- * column; node columns scroll horizontally when they overflow. The filter
- * input narrows which node columns are shown (by name or role).
+ * (CPU, Memory, then every discovered extended-resource key) is a row, read
+ * top-to-bottom. Node metadata (Status, Roles, Age) lives in each node's
+ * column header. The first column is a sticky label column; node columns
+ * scroll horizontally when they overflow. The filter input narrows which
+ * node columns are shown (by name or role).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Per-node table rendered below the aggregate panel. Fixed columns
* (Name, Status, Roles, CPU, Memory) plus one column per full
* extended-resource key found in the cluster, then Age. Headers click
* to sort; default sort is Name ascending. A filter input above the
* table filters by name and roles substring.
* Per-node table, transposed: each NODE is a column and each attribute
* (Status, Roles, CPU, Memory, every discovered extended-resource key, then
* Age) is a row, read top-to-bottom. The first column is a sticky label
* column; node columns scroll horizontally when they overflow. The filter
* input narrows which node columns are shown (by name or role).
*
* NotReady nodes show em dashes for CPU / Memory because status.capacity
* stops being authoritative; the rest of the row remains visible so the
* row remains a useful pointer for the operator. When pods-list failed
* cluster-wide, Requested values in every cell are replaced by an em
* dash with a tooltip explaining the missing permission.
* stops being authoritative. When the cluster-wide pods list failed,
* Requested figures are replaced by an em dash with an explanatory tooltip.
*/
/**
* Per-node table, transposed: each NODE is a column and each attribute
* (CPU, Memory, then every discovered extended-resource key) is a row, read
* top-to-bottom. Node metadata (Status, Roles, Age) lives in each node's
* column header. The first column is a sticky label column; node columns
* scroll horizontally when they overflow. The filter input narrows which
* node columns are shown (by name or role).
*/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/components/cluster-usage/ClusterUsageTable.tsx` around lines
138 - 148, Update the top doc comment in ClusterUsageTable.tsx to reflect the
refactor that moved Status, Roles, and Age into each node's column header (see
the code that renders node headers around where Status/Roles/Age are created) so
it no longer says those are rows; explicitly state that only CPU, Memory, and
discovered extended-resource keys are rendered as attributeRows and that
Status/Roles/Age appear in the per-node header columns and the first column
remains a sticky label column used for attribute row labels.

Comment thread apps/console/src/routes/AdminPage.tsx Outdated
Comment thread apps/console/src/routes/ClusterUsageResourcePage.tsx Outdated
Andrei Kvapil (kvaps) and others added 7 commits June 2, 2026 01:59
…e fix)

The cluster.x-k8s.io/cluster-name special-case in workloadOwner was a UI
band-aid for worker-node VMs that lacked lineage labels. cozystack#2779
now stamps apps.cozystack.io/application.{group,kind,name} on the worker
VM template, so those pods carry the standard lineage labels and the
generic workloadOwner path resolves them — the special-case is dead code.

This reverts commit d3fd9ac.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The resource-consumer drill-down linked each workload to its application's
overview page. Point the link at the app's Workloads tab instead, so a
click lands directly on the workloads of the selected resource.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The Admin portal lets a user in if they can use either area, so a backup-only
operator (backupclasses/update but not nodes/list) passed the portal gate and
could open the Capacity pages by direct URL — some of which then issued
cluster-wide pod/PVC lists they were never gated for. Wrap the capacity/* routes
in a CapacityAdminGuard that mirrors the Backup Classes guard and gates on
nodes/list, and route useAdminAccess through the shared useClusterUsageAccess
hook so the gate and the guard agree.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
ClusterStorageSection handled only loading and empty states, so a failed or
forbidden PersistentVolumeClaim list rendered as "No persistent volume claims
found" — indistinguishable from an actually-empty cluster. Add an error branch
that shows a permission notice on 403 and the failure message otherwise.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
ClusterUsageAggregates now renders the resources table plus ClusterUsageGauges
and no longer uses ResourceCard, leaving the component and its test with no
production consumer. Remove both.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
workloadOwner also tries the app.kubernetes.io/name label between instance and
the bare-name fallback; the ClusterUsageResourcePage comment omitted it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…egate

Clicking a resource's cluster-wide Requested figure opened a drill-down that
counted differently: it summed requests-or-limits, with no scheduled or
terminal-phase filter, so a limits-only or terminal/unscheduled pod inflated
the breakdown that was meant to itemize that number. Share the aggregate's
inclusion rule (scheduled to a known node, non-terminal, requests only) via a
podCountsTowardRequested helper and apply it in both places. The drill-down
stays tenant-scoped, now stated in the copy as the tenant portion of the
cluster figure rather than silently disagreeing with it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin portal: lifts the cluster-wide operator areas (Capacity — Cluster/Nodes/Storage — and Backup Classes) out of the tenant Console into a dedicated /admin portal, with per-resource and per-StorageClass consumer drill-downs. typecheck and the full vitest suite (275 tests) pass. Each area is gated by its own fail-closed SSAR guard (nodes/list, backupclasses/update), the cross-area direct-URL hole is closed and tested, the resource drill-down reconciles with the cluster aggregate via a shared inclusion rule, and the storage panel distinguishes a forbidden list from an empty one. LGTM.

@lexfrei Aleksei Sviridkin (lexfrei) merged commit df83d2f into main Jun 2, 2026
4 of 5 checks passed
@lexfrei Aleksei Sviridkin (lexfrei) deleted the feat/admin-portal branch June 2, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/console Issues or PRs related to apps/console — routes, detail pages, marketplace, command palette kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants