Skip to content

Add a CloudNativePG workspace: fleet triage, protection evidence, declarations and composed CNPG detail - #1921

Merged
nadaverell merged 19 commits into
mainfrom
feature/cnpg-workspace
Oct 7, 2026
Merged

nadaverell merged 19 commits into
mainfrom
feature/cnpg-workspace

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

CloudNativePG support in Radar today is ten separate CRD lists and renderers. Answering "which Postgres cluster needs attention, why, and what do I look at next" means stitching Clusters, Backups, ObjectStores, declarations and Pods together by hand. This adds a CloudNativePG workspace inside Resources that does that stitching. It covers fleet triage, recovery evidence, declared vs reconciled state, pooling and the operator. Every CNPG object gets one composed detail, whichever way you reach it.

The workspace is read-only. Every value it shows is something the cluster actually reports, labelled with its source. When something isn't reported it says so, never zero, "none" or green. The full contract is in docs/cnpg.md.

What changed

Workspace screens (/cnpg/*, inside Resources)

The Resources sidebar's CloudNativePG group gains a Workspace block above the exact kinds. Kinds sit under a collapsible "Resource kinds" block, grouped by API group.

  • Overview is the cluster fleet, defaulting to Needs attention. It shows instance pills, replication, protection, declarations, PG version and the top problem, with problem-category chips, search and a namespace chip. Each row inspects in the drawer and has explicit Logs and Open → actions.

  • Protection keeps these as separate facts per cluster, each with its source:

    • schedule
    • destination
    • last successful backup (newest of Backup CRs, ObjectStore status and in-tree status)
    • WAL archiving
    • recovery window: the earliest recoverable point from ObjectStore status, reaching the newest archived WAL. It reads "not advancing" only while WAL archiving fails; ObjectStore pages describe backup outcomes as recorded in status (a success whose status update failed is missing there), never as complete history
    • restore validation ("Unknown: no access to Backups" when the Backup a recovery names can't be read)

    It also lists failed backups from the last 7 days, destinations (ObjectStore upload health, labelled inferred with its evidence) and schedules. Restore validation is never green: Kubernetes records no restore tests.

  • Declarations lists Databases, Publications, Subscriptions and managed roles by cluster. It separates applied, not applied and pending, and shows controller errors and the GitOps source ("not recorded" when there isn't one).

  • Pooling lists Poolers and the clusters they front. Connection pressure reads "Not measured".

  • Operator shows operator and plugin Deployments with versions and readiness, image catalogs and their visible users, and operator configuration. Secret contents are never read.

One composed detail per object

  • WorkloadView gains an optional composed summary. For CNPG kinds, Overview is the summary (facts grouped by task, e.g. Backup → Outcome / Relationships) and the existing renderer moves to a Spec & status tab, so nothing repeats.
  • Every CNPG kind's full detail lives at /cnpg/<plural>/<ns|_>/<name>. You reach it from Open, from drawer expand, or by redirect from /workload/.... It keeps the workspace sidebar with the object nested under its home destination.
  • A Cluster adds three tabs: Protection; Activity, which replaces Timeline and covers the Cluster, its instance Pods and every CNPG object attributed to it, including deleted ones; and merged instance Logs, with role labels and parsed CNPG JSON levels (the PostgreSQL record.error_severity is read by the shared utils/log-level.ts, so every log viewer levels CNPG lines the same way). ?pod= preselects an instance.
  • The CNPG routes, gates and coverage states are listed in docs/cnpg.md#api; CLAUDE.md points there.

Navigation

  • Drawer: one drawer, URL-backed by ?drawer=kind:group:ns:name. Links inside it build a trail with a "← previous object" link in the drawer shell.
  • Return vs location: a drilldown carries a return label in history state ("← CloudNativePG Overview", "← Clusters"), kept across tab changes. Sidebar hops carry none. A crumb always names the object's place, so a fresh tab has a parent without a made-up previous task.
  • Context guard: detail URLs carry ctx=<kube context>, added on first view. After a context switch the page says " is not in ", with Switch back and Go to Overview, instead of opening a same-named object from another cluster.

Backend

  • GET /api/cnpg/workspace returns every CNPG kind plus instance Pods, authorized per kind, with coverage states: full, partial (including the allowed namespaces), denied, syncing, error or notInstalled.
    • Namespaced kinds fall back per namespace. ClusterImageCatalog needs a cluster-scope list.
    • Instance Pods must be controller-owned by the visible Cluster's UID.
    • Issues come from the Issues engine (flat, so Pod evidence only reaches callers who can list Pods), along with the cnpgNoDeclarativeBackup audit finding. Both are withheld where coverage is missing.
    • Denied namespaces are named only when the caller supplied the namespace list.
  • GET /api/cnpg/operator discovers operator and plugin workloads and their config references.
    • It ignores the namespace view filter, because the operator lives in its own namespace.
    • ConfigMap data is returned only with get configmaps; Secrets are never read.
  • GET /api/cnpg/clusters/{ns}/{name}/logs (plus /logs/stream) returns merged logs from owner-validated instance Pods. It is gated on get clusters, list pods and get pods/log.
  • GET /api/cnpg/clusters/{ns}/{name}/activity returns timeline events. Each kind is gated on list <kind>; Event rows additionally need list events.
  • Timeline attribution: ExtractLabels now retains cnpg.io/cluster (or spec.cluster.name) on CNPG objects and Pods, so deleted Backups and declarations stay attributed. No storage schema change.

Shared package (@skyhook-io/k8s-ui), all additive

  • ResourcesSidebar: optional categoryWorkspaces, with a pass-through sidebarCategoryWorkspaces on ResourcesView.
  • WorkloadView: optional renderSummary and extraTabs, plus a 'spec' value in WorkloadTabType.
  • WorkloadLogsViewer: optional initialPods.
  • New components/cnpg: buildCNPGFleet, the relation helpers and the summaries.

Consumers that pass none of these props see no change. That includes Radar Hub, which only needs to opt in if it wants the workspace.

Worth reviewing closely

  • Per-kind authorization in internal/server/cnpg_workspace.go: coverage, issue withholding, and not naming namespaces.
  • The certainty table in docs/cnpg.md, and packages/k8s-ui/src/components/cnpg/workspace.ts + relations.ts, which implement it.
  • The ?drawer= two-way sync in web/src/components/cnpg/CNPGView.tsx.
  • Context-switch handling in App.tsx, which preserves ctx on /cnpg/ routes.

Not included (needs a product/RBAC decision first)

  • Runtime data: replication lag, sessions, locks, WAL and slots via the instance manager (pods/proxy) and/or Prometheus. Replication reads "lag unknown" until then.
  • Write actions: Backup now, Switchover, Restart, Reload, Hibernate/Rehydrate, Restore to new cluster. No disabled placeholders are rendered.
  • Pooler connection pressure, which needs PgBouncer metrics.

These ship in #1922, stacked on this PR.

Testing

  • Up to date with main; CI is green on the head.
  • make tsc passes.
  • go test ./internal/server/ ./internal/timeline/ ./internal/k8s/ and pkg/timeline pass, including new handler tests for:
    • per-kind coverage, partial and denied namespaces, ClusterImageCatalog scope and kind collisions (Velero Backup, CAPI Cluster)
    • Pod evidence gating and instance ownership by UID
    • operator discovery and config gating
    • logs parsing, filtering and 403s
    • activity attribution of deleted children and per-kind and Event gating
  • k8s-ui vitest passes (4078 tests), including fleet derivation, relations, summaries and the sidebar workspace. Web vitest passes (1704 tests), including routes and the drawer trail.
  • make test: one unrelated cmd/desktop env test (TestEnrichEnvPrecedenceAndDiagnostics) failed under the full parallel run and passes on its own; nothing in cmd/ changed.
  • Visual: I ran against make cnpg-demo and walked these paths:
    • fleet → drawer → full Cluster page → Logs → return
    • Resources › Cluster → expand → same page → return to the list with the drawer restored
    • Protection → ObjectStore drawer → Cluster → trail back
    • Declarations with controller errors
    • Operator with the frozen demo operator scaled to 0
    • Activity including Backups
    • a context-mismatched link showing "not in this context"

Note

Medium Risk
New RBAC-gated read APIs (workspace per-kind coverage, pod logs, timeline activity) and log streaming increase authorization surface area, though behavior is read-only and heavily tested for partial/denied responses.

Overview
Adds a read-only CloudNativePG workspace under Resources (/cnpg/*) that stitches fleet triage, protection evidence, declarations, pooling, and operator views from one per-kind API payload, with composed per-kind detail pages (Overview summary + Spec & status) and Cluster-specific Protection, Activity, and merged instance Logs.

Backend: New GET /api/cnpg/workspace (per-kind coverage, issues, audit, owner-validated instance Pods), GET /api/cnpg/operator (ignores namespace view filter; ConfigMaps only with get), cluster logs (snapshot + SSE with CNPG JSON parsing) and activity (timeline with deleted-child attribution). Routes are registered in server.go; timeline ingestion retains cnpg.io/cluster for attribution.

Frontend (@skyhook-io/k8s-ui): New components/cnpg (fleet derivation, relations, certainty-aware summaries), optional WorkloadView summary/extra tabs, sidebar workspace block, drawer/ctx navigation patterns documented in docs/cnpg.md (also linked from CLAUDE.md and integrations.md).

Tests: Extensive server tests for coverage, RBAC withholding, logs/activity/stream behavior; vitest for CNPG log level detection.

Reviewed by Cursor Bugbot for commit e860514. Bugbot is set up for automated code reviews on this repo. Configure here.

… composed Cluster summary

- GET /api/cnpg/workspace: every CNPG kind plus owner-validated instance Pods,
  authorized per kind (namespaced kinds fall back per namespace; ClusterImageCatalog
  needs a cluster-scope list), with coverage states, CNPG issues from the Issues
  engine and the no-schedule audit finding withheld where coverage is missing.
- k8s-ui: buildCNPGFleet derives per-cluster facts (instances, replication,
  protection as separate schedule/destination/last-backup/WAL/recovery-window/
  restore-validation facts, declarations) without inventing values: replication
  lag is unknown without runtime data, restore validation is never green, and
  unreadable kinds read "No access" rather than none.
- ResourcesSidebar gains optional category workspaces (destinations above a
  collapsible Resource kinds block, grouped by API group).
- WorkloadView gains an optional composed summary: Overview shows it and the
  kind's renderer moves to a Spec & status tab.
- /cnpg: Overview fleet with Needs attention / All, problem-category chips,
  search, namespace chip and a URL-backed drawer (?drawer=kind:group:ns:name).
…ens and composed summaries

Screens (/cnpg/protection, /declarations, /pooling, /operator):
- Protection keeps schedule, destination, last successful backup (with its
  source), WAL archiving, recovery window and restore validation as separate
  facts; ObjectStore upload health is labelled inferred with its evidence.
- Declarations groups Databases, Publications, Subscriptions and managed roles
  by cluster, separating applied, not applied and pending, with controller
  errors and GitOps source.
- Pooling lists Poolers with readiness; connection pressure reads "Not
  measured" until PgBouncer metrics are read.
- Operator shows operator/plugin workloads and versions, image catalogs and
  their visible users, and operator configuration (Secret contents never read).

Composed Overview summaries (Spec & status keeps the existing renderers) for
Backup, ScheduledBackup, ObjectStore, Database, Publication, Subscription,
Pooler and image catalogs; drawer trail back-link on workspace screens.

GET /api/cnpg/operator discovers operator and plugin Deployments and their
config references, gated per namespace on list deployments/services and get
configmaps; it does not follow the namespace view filter.

Workspace hardening from review:
- issues are read flat, so Pod evidence only reaches callers with Pod access;
- instance Pods must be controller-owned by the visible Cluster's UID;
- partial coverage names denied namespaces only when the caller supplied the
  candidates, and always reports the allowed ones;
- unobserved managed roles are pending, unreadable schedules or Poolers are
  unknown rather than absent, restore evidence requires a ready restored
  cluster resolved by exact reference, and an unreadable Cluster list is not
  reported as "no clusters".
…ctivity

- Every CNPG kind now has a CNPG-framed full detail at /cnpg/<plural>/<ns|_>/<name>,
  reached from row Open, drawer expand and a redirect from /workload/...: the
  workspace sidebar stays highlighted with the object nested under its home, a
  return link names the page the drilldown came from (history state, kept
  across tab changes), and a crumb names the object's place.
- Detail URLs pin the kube context (ctx=, added on first view). After a context
  switch the page says the object is not in the new context and offers
  Switch back / Go to Overview instead of loading a same-named object.
- A Cluster adds Protection (its recovery evidence), Activity (replacing
  Timeline) and merged instance Logs; WorkloadView gains additive extraTabs and
  the logs viewer an initialPods selection.
- GET /api/cnpg/clusters/{ns}/{name}/logs (+/stream): logs from controller-owned
  instance Pods, gated on get clusters, list pods and get pods/log, with CNPG's
  JSON lines parsed into level/logger/message.
- GET /api/cnpg/clusters/{ns}/{name}/activity: timeline events for the Cluster,
  its instance Pods and CNPG objects attributed to it, per-kind gated. The
  timeline now records the owning cluster (cnpg.io/cluster label or
  spec.cluster.name) at ingestion so deleted Backups and declarations stay
  attributed; earlier history is reported as incomplete.
- The drawer trail back link moves to the drawer shell so it survives hops to
  Pods and Secrets.

Review fixes on the workspace screens and summaries: empty states follow each
kind's coverage, Declarations separates not applied from pending, GitOps
provenance reads "not recorded" instead of "applied directly", ObjectStore no
longer asserts recoverability, a Backup's destination inferred from the
Cluster's current config is labelled, schedule runs require the owner UID,
partial catalog coverage keeps the known users, and CNPG's six-field cron is
shown verbatim.

Docs: docs/cnpg.md (destinations, navigation, certainty table, access).
… redirects

- Activity rows from Kubernetes Events now also require list events in the
  namespace; they carry their subject's kind, so the subject check alone let
  Event messages through.
- The /workload redirect to a CNPG detail keeps ctx, pod and every other
  parameter, and maps a Cluster's timeline tab to Activity.
- The drawer trail back link keeps the page's return state.
- Activity describes its earliest linked child event as a recorded boundary,
  not a completeness guarantee.
@nadaverell
nadaverell requested a review from hisco as a code owner September 28, 2026 23:54
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add a read-only CloudNativePG workspace and composed object details

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add fleet, protection, declarations, pooling, and operator views for task-oriented CloudNativePG
 triage.
• Compose CNPG details from sourced evidence, distinguishing missing data from healthy or empty
 states.
• Authorize evidence per kind, add cluster activity and logs, and guard details across context
 switches.
Diagram

graph TD
  K["Kubernetes cache"] --> A["CNPG endpoints"] --> M["Fleet derivation"] --> S["Workspace screens"] --> D["CNPG detail"] --> V["Shared detail view"]
  I["Issues engine"] --> A
  T["Timeline store"] --> A
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Compose existing resource-list APIs in the browser
  • ➕ Avoids a dedicated aggregate endpoint.
  • ➖ Existing lists cannot reliably distinguish denied access from an empty kind.
  • ➖ Pod and issue evidence would still need independent authorization.
2. Derive all workspace facts on the server
  • ➕ Centralizes evidence interpretation.
  • ➖ Couples display-specific facts to Go handlers.
  • ➖ Reduces reuse of the additive shared UI model.

Recommendation: Keep the per-kind-authorized aggregate endpoint and shared pure derivations. This preserves the distinction between unreadable and absent evidence without duplicating the Issues engine. Review partial coverage and drawer/context transitions closely.

Files changed (60) +9741 / -73

Enhancement (45) +7079 / -72
cnpg_cluster_activity.goServe authorized CNPG cluster activity +238/-0

Serve authorized CNPG cluster activity

• Attributes retained Cluster, Pod, and child-object rows, including deleted children, while gating each kind and Kubernetes Events.

internal/server/cnpg_cluster_activity.go

cnpg_cluster_logs.goMerge owner-validated instance logs +465/-0

Merge owner-validated instance logs

• Adds bounded snapshots and SSE following with RBAC gates, Pod filtering, role labels, and CNPG JSON parsing.

internal/server/cnpg_cluster_logs.go

cnpg_operator.goDiscover operator, plugins, and configuration references +366/-0

Discover operator, plugins, and configuration references

• Reads authorized Deployments and Services; exposes ConfigMap data only with get permission and never reads Secrets.

internal/server/cnpg_operator.go

cnpg_workspace.goProvide per-kind-authorized CNPG workspace data +677/-0

Provide per-kind-authorized CNPG workspace data

• Lists CNPG kinds and owner-validated Pods with coverage, bounded Backup history, visibility-filtered issues, and guarded audit findings.

internal/server/cnpg_workspace.go

server.goRegister CNPG workspace and cluster endpoints +5/-0

Register CNPG workspace and cluster endpoints

• Adds workspace, operator, activity, log snapshot, and SSE stream routes.

internal/server/server.go

workload_logs.goAllow parsed fields on shared log entries +5/-1

Allow parsed fields on shared log entries

• Adds optional level, logger, and message fields and clarifies the pods/log permission error.

internal/server/workload_logs.go

CNPGBackupSummary.tsxCompose Backup and ScheduledBackup overviews +223/-0

Compose Backup and ScheduledBackup overviews

• Groups outcomes, timing, destinations, schedule relationships, and recent runs.

packages/k8s-ui/src/components/cnpg/CNPGBackupSummary.tsx

CNPGClusterSummary.tsxCompose cluster problems and sourced facts +222/-0

Compose cluster problems and sourced facts

• Displays triage evidence, instances, protection and declaration facts, relationships, and optional drawer actions.

packages/k8s-ui/src/components/cnpg/CNPGClusterSummary.tsx

CNPGDeclarativeSummary.tsxCompose declarative-resource details +264/-0

Compose declarative-resource details

• Separates declared state, reconciliation evidence, relationships, and GitOps provenance for Databases, Publications, and Subscriptions.

packages/k8s-ui/src/components/cnpg/CNPGDeclarativeSummary.tsx

CNPGImageCatalogSummary.tsxShow catalog images and visible consumers +80/-0

Show catalog images and visible consumers

• Lists PostgreSQL image versions and linked Clusters, accounting for catalog scope and incomplete visibility.

packages/k8s-ui/src/components/cnpg/CNPGImageCatalogSummary.tsx

CNPGObjectStoreSummary.tsxExplain ObjectStore recovery and inferred health +176/-0

Explain ObjectStore recovery and inferred health

• Shows per-cluster upload evidence, recovery windows, destination, and credential references without asserting direct ObjectStore health.

packages/k8s-ui/src/components/cnpg/CNPGObjectStoreSummary.tsx

CNPGPoolerSummary.tsxCompose Pooler status and routing overview +70/-0

Compose Pooler status and routing overview

• Shows reported instance counts, target Cluster, routing and Deployment links, and unmeasured connection pressure.

packages/k8s-ui/src/components/cnpg/CNPGPoolerSummary.tsx

CNPGSharedSummary.tsxShare CNPG summary evidence components +88/-0

Share CNPG summary evidence components

• Provides issue callouts, statuses, time formatting, and coverage-aware Cluster links.

packages/k8s-ui/src/components/cnpg/CNPGSharedSummary.tsx

index.tsExport CNPG workspace components +8/-0

Export CNPG workspace components

• Exposes fleet derivations, summary primitives, and object summaries.

packages/k8s-ui/src/components/cnpg/index.ts

primitives.tsxAdd sourced fact and problem primitives +134/-0

Add sourced fact and problem primitives

• Provides fact grids, tones, source tooltips, relationship links, and evidence callouts.

packages/k8s-ui/src/components/cnpg/primitives.tsx

relations.tsDerive evidence-backed CNPG relationships +417/-0

Derive evidence-backed CNPG relationships

• Resolves links among Backups, schedules, stores, declarations, Clusters, and catalogs while respecting scope and provenance.

packages/k8s-ui/src/components/cnpg/relations.ts

workspace.tsBuild coverage-aware CNPG fleet facts +683/-0

Build coverage-aware CNPG fleet facts

• Derives sourced protection, replication, declaration, instance, and problem facts without treating missing values as healthy.

packages/k8s-ui/src/components/cnpg/workspace.ts

WorkloadLogsViewer.tsxSupport initial Pod selection in logs +11/-3

Support initial Pod selection in logs

• Adds optional initialPods selection for instance-focused Cluster log links.

packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx

ResourcesSidebar.tsxAdd optional task destinations within resource categories +183/-11

Add optional task destinations within resource categories

• Places workspace links above collapsible, API-grouped kinds without changing consumers that omit the prop.

packages/k8s-ui/src/components/resources/ResourcesSidebar.tsx

ResourcesView.tsxPass category workspaces into the sidebar +5/-1

Pass category workspaces into the sidebar

• Adds an optional ResourcesView prop and forwards it to ResourcesSidebar.

packages/k8s-ui/src/components/resources/ResourcesView.tsx

index.tsExport workspace sidebar types +1/-1

Export workspace sidebar types

• Makes destination and category types available to package consumers.

packages/k8s-ui/src/components/resources/index.ts

WorkloadView.tsxSupport composed summaries and extra detail tabs +90/-5

Support composed summaries and extra detail tabs

• Adds optional summary rendering, a Spec & status tab, and replaceable or insertable expanded tabs.

packages/k8s-ui/src/components/workload/WorkloadView.tsx

index.tsExport extra-tab contract +1/-0

Export extra-tab contract

• Exposes WorkloadExtraTab for domain-specific detail tabs.

packages/k8s-ui/src/components/workload/index.ts

index.tsPublish the shared CNPG module +3/-0

Publish the shared CNPG module

• Exports the workspace model and composed summaries at the package root.

packages/k8s-ui/src/index.ts

converter.goRetain CNPG Cluster identity on timeline rows +33/-3

Retain CNPG Cluster identity on timeline rows

• Extracts cnpg.io/cluster from CNPG labels or spec.cluster.name and from Pods, preserving attribution after deletion.

pkg/timeline/converter.go

App.tsxIntegrate CNPG routing and context-safe expansion +44/-5

Integrate CNPG routing and context-safe expansion

• Mounts the workspace under Resources, invalidates its query on CNPG changes, routes drawer expansion, and preserves pinned context.

web/src/App.tsx

cnpg.tsAdd typed CNPG workspace queries +95/-0

Add typed CNPG workspace queries

• Defines cached clients for workspace, operator, and Cluster activity responses.

web/src/api/cnpg.ts

CNPGClusterActivity.tsxDisplay attributed Cluster activity +49/-0

Display attributed Cluster activity

• Shows selectable time ranges and explains attribution gaps and truncated history.

web/src/components/cnpg/CNPGClusterActivity.tsx

CNPGClusterLogs.tsxConnect Cluster detail to merged instance logs +59/-0

Connect Cluster detail to merged instance logs

• Adapts the shared viewer to CNPG snapshots and SSE, with optional Pod preselection.

web/src/components/cnpg/CNPGClusterLogs.tsx

CNPGDeclarations.tsxTriage declared versus reconciled resources +311/-0

Triage declared versus reconciled resources

• Groups declarations and managed roles by Cluster, exposing pending states, controller errors, and GitOps sources.

web/src/components/cnpg/CNPGDeclarations.tsx

CNPGDetailPage.tsxFrame CNPG details within the workspace +228/-0

Frame CNPG details within the workspace

• Provides context-guarded details, home breadcrumbs and return state, plus Cluster Protection and Activity tabs.

web/src/components/cnpg/CNPGDetailPage.tsx

CNPGDrawerTrail.tsxNavigate backward through inspected objects +37/-0

Navigate backward through inspected objects

• Displays a previous-object action using the URL-backed CNPG drawer trail.

web/src/components/cnpg/CNPGDrawerTrail.tsx

CNPGOperator.tsxShow operator readiness and catalog usage +230/-0

Show operator readiness and catalog usage

• Displays workloads, configuration references, catalogs, visible users, and coverage gaps.

web/src/components/cnpg/CNPGOperator.tsx

CNPGOverview.tsxAdd searchable CNPG fleet triage +307/-0

Add searchable CNPG fleet triage

• Lists Clusters with instance and operational evidence, attention and category filters, and explicit drilldowns.

web/src/components/cnpg/CNPGOverview.tsx

CNPGPooling.tsxList Poolers and their target Clusters +116/-0

List Poolers and their target Clusters

• Shows routing, status, and instance counts while marking connection pressure unmeasured.

web/src/components/cnpg/CNPGPooling.tsx

CNPGProtection.tsxPresent recovery evidence by task +311/-0

Present recovery evidence by task

• Separates per-Cluster protection facts and lists recent failed Backups, destinations, and schedules.

web/src/components/cnpg/CNPGProtection.tsx

CNPGSummaryHost.tsxSelect composed summaries for CNPG kinds +143/-0

Select composed summaries for CNPG kinds

• Connects shared summaries to scoped workspace data and excludes same-named foreign kinds.

web/src/components/cnpg/CNPGSummaryHost.tsx

CNPGView.tsxCoordinate screens, sidebar, and URL-backed drawer +156/-0

Coordinate screens, sidebar, and URL-backed drawer

• Hosts workspace destinations and details with shared fleet data and two-way drawer synchronization.

web/src/components/cnpg/CNPGView.tsx

paths.tsBuild Cluster paths and return labels +13/-0

Build Cluster paths and return labels

• Centralizes Cluster detail URLs and labels for history-based drilldown returns.

web/src/components/cnpg/paths.ts

routes.tsDefine CNPG destinations and detail URLs +112/-0

Define CNPG destinations and detail URLs

• Maps kinds to homes and provides group-aware route parsing, context paths, and drawer-trail serialization.

web/src/components/cnpg/routes.ts

shared.tsxShare workspace screen states and controls +290/-0

Share workspace screen states and controls

• Provides loading and not-installed gates, coverage notices, filters, and reusable layout components.

web/src/components/cnpg/shared.tsx

useCNPGSidebarWorkspace.tsPopulate workspace destinations and counts +88/-0

Populate workspace destinations and counts

• Derives sidebar links and affected-Cluster counts from the same namespace-scoped fleet query as the screens.

web/src/components/cnpg/useCNPGSidebarWorkspace.ts

ResourceDetailDrawer.tsxShow CNPG navigation trail in the existing drawer +6/-0

Show CNPG navigation trail in the existing drawer

• Adds a previous-object control above drawer content on CNPG routes.

web/src/components/resources/ResourceDetailDrawer.tsx

ResourcesView.tsxExpose CNPG workspace from Resources +6/-38

Expose CNPG workspace from Resources

• Passes sidebar destinations to the shared view and moves resource-count querying into a reusable hook.

web/src/components/resources/ResourcesView.tsx

WorkloadView.tsxRoute CNPG objects to composed details +30/-4

Route CNPG objects to composed details

• Redirects generic CNPG links, supplies summaries and merged Cluster logs, and preserves return state across tabs.

web/src/components/workload/WorkloadView.tsx

Refactor (1) +49 / -0
useResourceCounts.tsShare sidebar resource-count querying +49/-0

Share sidebar resource-count querying

• Extracts the existing namespace-scoped count query for Resources and standalone CNPG sidebars.

web/src/hooks/useResourceCounts.ts

Tests (9) +2371 / -0
cnpg_cluster_history_test.goTest cluster logs and activity handlers +449/-0

Test cluster logs and activity handlers

• Exercises instance ownership, merged logs, historical attribution, and activity authorization.

internal/server/cnpg_cluster_history_test.go

cnpg_operator_test.goTest operator discovery and configuration gates +368/-0

Test operator discovery and configuration gates

• Covers identification, readiness, scope, and guarded configuration reads.

internal/server/cnpg_operator_test.go

cnpg_workspace_test.goTest workspace coverage and evidence isolation +584/-0

Test workspace coverage and evidence isolation

• Checks per-kind and namespace access, cluster-scoped catalogs, colliding API groups, Pod ownership, and evidence withholding.

internal/server/cnpg_workspace_test.go

CNPGObjectSummary.test.tsxTest rendered CNPG object overviews +231/-0

Test rendered CNPG object overviews

• Checks summary claims about backups, destinations, declarations, Poolers, catalogs, and ObjectStores.

packages/k8s-ui/src/components/cnpg/CNPGObjectSummary.test.tsx

relations.test.tsTest CNPG relationship and certainty helpers +304/-0

Test CNPG relationship and certainty helpers

• Validates group-aware links, schedule ownership, destinations, catalog users, reconciliation, and inferred health.

packages/k8s-ui/src/components/cnpg/relations.test.ts

workspace.test.tsTest fleet fact derivation +233/-0

Test fleet fact derivation

• Covers protection, replication uncertainty, declarations, access gaps, and issue-driven attention counts.

packages/k8s-ui/src/components/cnpg/workspace.test.ts

ResourcesSidebar.test.tsxTest category workspace sidebar +76/-0

Test category workspace sidebar

• Checks destinations, active-object nesting, collapsed kinds, zero-resource visibility, and API-group labels.

packages/k8s-ui/src/components/resources/ResourcesSidebar.test.tsx

converter_cnpg_test.goTest retained CNPG timeline attribution +66/-0

Test retained CNPG timeline attribution

• Checks supported groups, Pod forms, unrelated-kind exclusion, and attribution surviving tombstones.

pkg/timeline/converter_cnpg_test.go

routes.test.tsTest routes and drawer identity +60/-0

Test routes and drawer identity

• Covers destinations, detail kinds, context URLs, malformed trails, and API-group collisions.

web/src/components/cnpg/routes.test.ts

Documentation (5) +242 / -1
CLAUDE.mdDocument CNPG workspace contracts +5/-0

Document CNPG workspace contracts

• Links the certainty and navigation documentation and records the new endpoint access and attribution rules.

CLAUDE.md

README.mdList all supported CNPG kinds and workspace +1/-1

List all supported CNPG kinds and workspace

• Corrects the integration inventory and links to the new workspace.

README.md

cnpg.mdDefine workspace behavior and evidence certainty +66/-0

Define workspace behavior and evidence certainty

• Documents destinations, navigation, per-fact sources and unknown states, access rules, and deferred capabilities.

docs/cnpg.md

integrations.mdLink integration guidance to the workspace +2/-0

Link integration guidance to the workspace

• Introduces the workspace as the task-oriented complement to per-kind CloudNativePG views.

docs/integrations.md

CNPG_WORKSPACE.mdRecord the phased workspace design +168/-0

Record the phased workspace design

• Captures architecture, product decisions, risks, testing, deferred features, and design revisions.

docs/plans/CNPG_WORKSPACE.md

Comment thread packages/k8s-ui/src/components/cnpg/CNPGObjectSummary.test.tsx Fixed

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/cnpg/workspace.ts
Comment thread packages/k8s-ui/src/components/cnpg/workspace.ts
Comment thread internal/server/cnpg_cluster_activity.go
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Activity query failures lack server logs ✓ Resolved
Description
handleCNPGClusterActivity returns HTTP 500 for either failed timeline query without logging the
triggering err. When the initial history scan or the oldest-record lookup fails, the handler sends
err.Error() to the client but records no diagnostic entry on either path.
Code

internal/server/cnpg_cluster_activity.go[R146-147]

+	if err != nil {
+		s.writeError(w, http.StatusInternalServerError, err.Error())
Evidence
The new activity handler has two timeline-query error branches that write HTTP 500 without a
preceding log call; the cited rule requires a standardized log before each 500 response.

Rule 3036628: Log 500 errors with standardized module/action format before writing the response
internal/server/cnpg_cluster_activity.go[137-148]
internal/server/cnpg_cluster_activity.go[221-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Both timeline queries in the cluster activity handler can return HTTP 500 without a preceding diagnostic log.
## Fix Focus Areas
- internal/server/cnpg_cluster_activity.go[146-148]
- internal/server/cnpg_cluster_activity.go[229-231]
## Recommended Fix
Before each 500 response, log the triggering error with a `[cnpg] Failed to ... %s/%s: %v` message using the cluster namespace and name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stream setup failures lack server logs ✓ Resolved
Description
handleCNPGClusterLogsStream returns HTTP 500 when the response writer does not support flushing,
without first logging the failure. This path occurs after the cluster and its instance Pods are
selected, so the response alone does not leave a server-side diagnostic tied to their namespace and
name.
Code

internal/server/cnpg_cluster_logs.go[R369-372]

+	flusher, ok := w.(http.Flusher)
+	if !ok {
+		s.writeError(w, http.StatusInternalServerError, "streaming not supported")
+		return
Evidence
The added flusher check writes HTTP 500 directly when streaming is unsupported, with no preceding
log call as required by the cited rule.

Rule 3036628: Log 500 errors with standardized module/action format before writing the response
internal/server/cnpg_cluster_logs.go[369-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The cluster log stream handler sends a 500 when its response writer cannot flush, without recording a diagnostic log.
## Fix Focus Areas
- internal/server/cnpg_cluster_logs.go[369-372]
## Recommended Fix
Log the unsupported-flusher failure before writing the 500, using a `[cnpg] Failed to ... %s/%s: %v` message with the cluster namespace and name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Recently failed backups disappear ✓ Resolved
Description
CNPGProtection filters the last-seven-days list using backupTime(), which prefers startedAt
and never checks stoppedAt. A backup that started more than seven days ago but failed recently is
retained by the backend's completion-time window yet omitted from the failed-backups table.
Code

web/src/components/cnpg/CNPGProtection.tsx[R43-45]

+function backupTime(b: any): string | undefined {
+  return b?.status?.startedAt || b?.metadata?.creationTimestamp
+}
Evidence
The frontend applies its seven-day cutoff to a start-time helper, whereas the backend retains
settled backups using stop time first.

web/src/components/cnpg/CNPGProtection.tsx[43-45]
web/src/components/cnpg/CNPGProtection.tsx[98-110]
internal/server/cnpg_workspace.go[366-419]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Recently failed, long-running backups are excluded because the frontend filters by start time while the backend retains them by stop time.
## Fix Focus Areas
- web/src/components/cnpg/CNPGProtection.tsx[43-45]
- web/src/components/cnpg/CNPGProtection.tsx[98-110]
- internal/server/cnpg_workspace.go[366-375]
## Recommended Fix
Use `stoppedAt`, falling back to `startedAt` and creation time, for the failure cutoff and ordering. Keep the table's explicitly labelled Started column based on `startedAt`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (7)
4. Interrupted streams replay old log lines ✓ Resolved
Description
handleCNPGClusterLogsStream removes a finished log reader from active, then its five-second
discovery tick starts that reader again with the original tail and time parameters. If a Pod log
stream reaches EOF or has a transient read error while the Pod remains present, its recent lines are
appended again on every restart.
Code

internal/server/cnpg_cluster_logs.go[R400-403]

+				go func(podName, containerName, key string) {
+					defer active.Delete(key)
+					streamPodLogs(streamCtx, client, namespace, podName, containerName, query.tailLines, query.sinceSeconds, logCh)
+				}(pod.Name, c, key)
Evidence
The reader deletes its active entry on exit; the periodic start(currentPods) call starts it again
with unchanged query parameters, and the client appends each received event.

internal/server/cnpg_cluster_logs.go[390-407]
internal/server/cnpg_cluster_logs.go[426-463]
internal/server/workload_logs.go[632-679]
packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[204-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A terminated per-Pod stream is restarted with its initial tail, repeatedly duplicating entries in the live viewer.
## Fix Focus Areas
- internal/server/cnpg_cluster_logs.go[390-407]
- internal/server/cnpg_cluster_logs.go[426-463]
- internal/server/workload_logs.go[632-679]
## Recommended Fix
Track each reader's last delivered timestamp and resume after it, deduplicating any overlap, or avoid automatically restarting finished readers with the original tail.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Live logs lose instance role labels ✓ Resolved
Description
WorkloadLogsViewer's live onLog handler discards the sourceLabel that the new cluster stream
puts on each entry. Snapshot lines receive their primary or replica label, but lines arriving after
streaming starts fall back to a Pod-name fragment.
Code

web/src/components/cnpg/CNPGClusterLogs.tsx[R45-48]

+
+  return (
+    <div className="h-full">
+      <WorkloadLogsViewer
Evidence
The server sends a role as SourceLabel, but the live handler constructs a new entry without it;
the log renderer uses that field for its displayed source.

internal/server/cnpg_cluster_logs.go[419-425]
packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[141-152]
packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[204-218]
packages/k8s-ui/src/components/logs/LogCore.tsx[1187-1189]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new CNPG stream supplies primary and replica labels, but the shared viewer drops them from live entries.
## Fix Focus Areas
- web/src/components/cnpg/CNPGClusterLogs.tsx[45-55]
- packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[204-218]
- internal/server/cnpg_cluster_logs.go[419-425]
## Recommended Fix
Pass `data.sourceLabel` into the viewer's buffered live entry and cover both snapshot and streaming labels in a test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Database errors can show as info logs ✓ Resolved
Description
annotateCNPGLogEntry extracts PostgreSQL's record.error_severity, but the new viewer feeds only
raw content into a buffer that independently detects severity. When a wrapped record has a
top-level info level and a nested error severity, the displayed log level follows the top-level
value instead of the extracted database severity.
Code

internal/server/cnpg_cluster_logs.go[R218-225]

+		}
+		if rec.Record.Message != "" {
+			message = rec.Record.Message
+		}
+	}
+	if errText := cnpgLogErrorText(rec.Error); errText != "" {
+		if message == "" {
+			message = errText
Evidence
The backend explicitly overrides level from the nested record, while both viewer paths omit that
parsed field and the buffer detects only top-level JSON severity fields.

internal/server/cnpg_cluster_logs.go[188-233]
packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[141-152]
packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[204-218]
packages/k8s-ui/src/components/logs/useLogBuffer.ts[30-55]
packages/k8s-ui/src/components/logs/useLogBuffer.ts[82-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The backend extracts nested PostgreSQL severity, but the viewer discards it and reclassifies the raw JSON.
## Fix Focus Areas
- internal/server/cnpg_cluster_logs.go[207-233]
- packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[141-152]
- packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx[204-218]
- packages/k8s-ui/src/components/logs/useLogBuffer.ts[30-55]
## Recommended Fix
Carry the server's parsed severity through snapshot and streaming entries, and let the log buffer prefer that supplied severity to raw-content detection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unrelated plugins look like restorations ✓ Resolved
Description
restoreValidationFact matches an external recovery plugin by barmanObjectName and server name
without checking the plugin's identity. Another plugin carrying those parameter names can make a
source cluster display that recovery was declared or completed from its backups.
Code

packages/k8s-ui/src/components/cnpg/workspace.ts[R416-418]

+      const ext = (c.spec?.externalClusters ?? []).find((e: any) => e?.name === sourceName)
+      const params = ext?.plugin?.parameters
+      if (store && params?.barmanObjectName === store && (params?.serverName || sourceName) === server) return true
Evidence
The source backup plugin is identified by name, but the external recovery plugin is not; the
parameter-only match directly produces the restore fact.

packages/k8s-ui/src/components/cnpg/workspace.ts[405-440]
packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts[440-453]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Restore evidence is attributed to a source using parameter matches without verifying that the recovery plugin is barman-cloud.
## Fix Focus Areas
- packages/k8s-ui/src/components/cnpg/workspace.ts[410-424]
- packages/k8s-ui/src/components/resources/resource-utils-cnpg.ts[440-453]
## Recommended Fix
Require the matching external cluster's plugin name to be the barman-cloud plugin before treating its parameters as evidence of recovery from this source.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Plugin rows can reuse the same key ✓ Resolved
Description
CNPGOperator keys component rows solely by namespace and deployment, while unmatched plugin
Services produce components with no deployment. Two such Services in one namespace yield identical
React keys, making their rows unstable when the table updates.
Code

web/src/components/cnpg/CNPGOperator.tsx[R118-120]

+              rows={op.components}
+              rowKey={(c) => `${c.namespace}/${c.deployment}`}
+              rowResource={(c) => (c.deployment ? { kind: 'deployments', group: 'apps', namespace: c.namespace, name: c.deployment } : null)}
Evidence
The server appends a separate fallback component for each unmatched Service without setting
Deployment; the table uses that empty field as its only per-namespace key component.

internal/server/cnpg_operator.go[115-133]
web/src/components/cnpg/CNPGOperator.tsx[118-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Multiple unmatched plugin Services in a namespace produce components with the same empty-deployment row key.
## Fix Focus Areas
- internal/server/cnpg_operator.go[115-133]
- web/src/components/cnpg/CNPGOperator.tsx[118-120]
## Recommended Fix
Include the originating Service name in fallback components and use it, together with namespace and role, for a stable unique table key.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Backup destinations show healthy on thin evidence ✓ Resolved
Description
inferStoreHealth returns 'Accepting uploads' with a healthy tone when any single user cluster has
healthy WAL archiving. Users whose archiving is unknown are ignored, not counted against the
verdict. So when one of a store's visible clusters has no ContinuousArchiving condition, or only a
subset of clusters is visible, the Protection screen shows green while the store's own detail view
says 'No failures reported' (unknown).
Code

web/src/components/cnpg/CNPGProtection.tsx[R76-78]

+    return { text: 'Accepting uploads', tone: 'healthy', evidence: `Inferred from WAL archiving on ${archiving.map((u) => u.name).join(', ')}` }
+  }
+  return { text: 'Unknown', tone: 'unknown', evidence: 'Its clusters report no archiving result yet' }
Evidence
On the Protection screen, the check that returns a healthy result only needs one user cluster to
report healthy archiving. walFact gives the 'unknown' tone to clusters with no ContinuousArchiving
condition, and those clusters get past both the failing check and the healthy check. The ObjectStore
detail in relations.ts requires every user to be healthy and otherwise returns unknown, so the two
views disagree.

web/src/components/cnpg/CNPGProtection.tsx[60-80]
packages/k8s-ui/src/components/cnpg/relations.ts[311-315]
packages/k8s-ui/src/components/cnpg/workspace.ts[389-394]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`inferStoreHealth` marks an ObjectStore 'Accepting uploads' (healthy) when at least one user cluster has healthy WAL archiving, even if other users report nothing. This breaks the rule that unknown is never shown as green, and it contradicts the ObjectStore detail logic in relations.ts.
## Fix Focus Areas
- web/src/components/cnpg/CNPGProtection.tsx[60-80]
- packages/k8s-ui/src/components/cnpg/relations.ts[294-315]
## Recommended Fix
Return the healthy verdict only when `users.every(u => u.protection.walArchiving.tone === 'healthy')`. Otherwise return an unknown tone such as 'No failures reported'. Better, reuse the relations.ts helper so the Protection screen and the ObjectStore detail share one inference.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Staging loses plugin backup destinations 🔗 Cross-repo conflict ≡ Correctness
Description
cnpgWorkspaceKinds adds Barman ObjectStore resources, but the vendored Radar chart in the
deployment repo grants CloudNativePG access only to postgresql.cnpg.io. When the Barman plugin is
installed in staging, its barmancloud.cnpg.io ObjectStores cannot be read, so the workspace cannot
show their backup destinations.
Code

internal/server/cnpg_workspace.go[67]

+	{key: "objectStores", group: cnpgBarmanGroup, kind: "ObjectStore", resource: "objectstores"},
Evidence
The PR adds barmancloud.cnpg.io ObjectStores to the workspace and checks list permission before
reading each kind. The deployment repo enables its CloudNativePG RBAC option, but the corresponding
vendored rule grants only postgresql.cnpg.io; unlike Radar's chart, it omits the Barman group.

radar -> deployment
internal/server/cnpg_workspace.go[57-68]
internal/server/cnpg_workspace.go[293-310]
deploy/helm/radar/templates/clusterrole.yaml[271-277]
External repo: skyhook-dev/deployment, argocd/addons/radar-staging/radar-staging/skh-nonprod/charts/radar/values.yaml [125-135]
External repo: skyhook-dev/deployment, argocd/addons/radar-staging/radar-staging/skh-nonprod/charts/radar/templates/clusterrole.yaml [232-236]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CNPG workspace reads Barman ObjectStores, but the deployment repo's vendored staging chart does not grant access to their API group.
## Fix Focus Areas
- internal/server/cnpg_workspace.go[57-68]
- /cross_repos/deployment/argocd/addons/radar-staging/radar-staging/skh-nonprod/charts/radar/templates/clusterrole.yaml[232-236]
## Recommended Fix
Update the deployment repo's vendored staging ClusterRole to grant read access to `barmancloud.cnpg.io` when CloudNativePG access is enabled, then deploy the chart alongside the workspace feature.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/cnpg_cluster_activity.go
Comment thread internal/server/cnpg_cluster_logs.go
Comment thread web/src/components/cnpg/CNPGProtection.tsx
Comment thread internal/server/cnpg_cluster_logs.go Outdated
Comment thread web/src/components/cnpg/CNPGClusterLogs.tsx
Comment thread internal/server/cnpg_cluster_logs.go
Comment thread packages/k8s-ui/src/components/cnpg/workspace.ts
Comment thread web/src/components/cnpg/CNPGOperator.tsx
Comment thread web/src/components/cnpg/CNPGProtection.tsx Outdated
Comment thread internal/server/cnpg_workspace.go
…severity

- Replication and ObjectStore-sourced facts read "No access" when Pods or
  ObjectStores are unreadable, instead of asserting absence.
- Restore evidence only counts barman-cloud plugin recovery sources.
- Inferred ObjectStore health is "Accepting uploads" only when every user
  cluster reports archiving; the failed-backup window uses completion time.
- Log level detection reads CloudNativePG's nested PostgreSQL severity, and
  live stream lines keep their instance role label.
- A restarted instance log stream resumes from the last delivered line instead
  of replaying its tail; activity Pod rows must match the live Cluster's UID;
  activity and stream failures are logged.
- Workspace components use the shared Tooltip rather than native titles.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/cnpg/workspace.ts Outdated
Conflicts:
- CLAUDE.md: main condensed the endpoint list; the CloudNativePG
  endpoints move to docs/cnpg.md#api behind a one-line pointer.
- useLogBuffer.ts: main moved level detection to utils/log-level.ts;
  the CloudNativePG record.error_severity rule moves with it into
  selectLevelField.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/cnpg/workspace.ts
CloudNativePG's record.error_severity is one of PostgreSQL's severity
names; another logger's numeric or custom record field no longer
overrides the line's own level.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/utils/log-level.ts
…nreadable Backups

- PostgreSQL writes every DEBUGn level as DEBUG in error_severity.
- Restore validation says the Backup a recovery names couldn't be read,
  instead of None recorded, when Backups are denied in the namespace.
Recovery replays archived WAL past the latest base backup, and no
status reports the newest recoverable point, so the window shows its
earliest point and reaches the newest archived WAL. It reads not
advancing only while WAL archiving fails; a failed base backup no
longer implies it.
Recovery replays archived WAL past the last base backup, so a failed
backup makes recovery slower, not shorter; only archiving stops it.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx Outdated
The failing-backups banner names servers whose cluster also stopped
archiving and drops the claim that recovery reaches the newest WAL, and
the row keeps its archiving-stopped note while backups fail.
With failures and no success the store holds nothing to restore from,
and retention isn't shown to move the earliest point.
The plugin keeps a successful backup when it fails to update the
ObjectStore status, so an absent success time leaves recoverability
unestablished rather than proving there is nothing to restore.
Status can miss a success whose update failed, so a failure is stated
as recorded after the last recorded success, and an entry without
timestamps reads as a recovery point not reported.
It is internal planning; docs/cnpg.md stops pointing at it.
nadaverell added a commit that referenced this pull request Oct 4, 2026
…workspace (#1969)

## Summary

The CloudNativePG workspace (#1921, #1922) carried a generic layer under
CNPG names: the facts and problem list on every summary, per-kind
coverage, the reviewed-action contract, grants, Prometheus series
attribution, and screen layout borrowed from Capacity's internals. This
PR moves that layer into shared, integration-neutral modules, so the
next workspace-style integration (Velero is the likely one) imports it
instead of copying CNPG.

Nothing here is released yet, so the three CNPG wire shapes that every
future workspace would copy are fixed now:
- grants are structured objects, not sentences;
- "not cached by Radar" is told apart from "no access";
- each object's GitOps manager comes from the server.

It also adds the version-skew gate the CNPG endpoints were missing.

Only mechanisms whose interface is already evident in today's code are
shared: each has two or more consumers, or is a contract every workspace
must follow. The rest is listed below as not shared yet.

## What changed

### Shared UI (`@skyhook-io/k8s-ui`)
- **`components/facts/`**, for any surface that shows observed values
(single-kind renderers included):
  - `Fact`, with `FactGrid`/`FactRow`/`FactValue`/`FactSource`
- `CertaintyGlyph`, moved from Capacity; `CapacityCertainty` stays as
the same type
  - `ManagedByText`
- **`components/problems/`**: `WorkspaceProblem<Category>` and
`ProblemCallout`/`ProblemList`/`ProblemMeta`, which take the workspace's
`rootKind` instead of a hard-coded `'Cluster'`, plus `OpenIssueContext`.
- **`ui/FoldSection`**: `SectionHeading`, `FoldSection`, `FoldSummary`.
- **Elsewhere in k8s-ui**:
  - `ui/RefLink` now uses the existing `ResourceRef`.
  - `toneTextClass` and `worseTone` live in `ui/status-tone`.
  - `utils/grant` adds `Grant`, `formatGrant` and `grantParts`.
- `issueReasonTitle` gives a reason one title on both the Issues page
and the workspace.
- **Kept CNPG-specific:** categories, ordering, wording and builders.
CNPG binds `WorkspaceProblem` to its own categories.
- **Badges:** CNPG badges now use `healthToSeverity` like the rest of
the app.
- **Buttons:** secondary buttons use a new `.btn-secondary` class
(documented in DESIGN.md) instead of ten hand-rolled class strings.
- **Sidebar:** `SidebarCategoryDestination.countLowerBound` renders a
count over partly read data as `≥N`, and its zero as unknown rather than
none.

### App (`web/`)
- **`components/workspace/`** holds the screen primitives Capacity and
CNPG share: `ScreenBody`, `ScreenEmptyState`, `Notice`, `Segments`,
`FilterChips`, `SectionTable` and its table classes,
`RefreshFailedNotice`, `GrantText`, and the text helpers. CNPG no longer
imports from `capacity/shared.tsx`.
- **`api/actions.ts`** is the client half of the action contract:
  - the `ActionCapability` and `ActionRequest` types
  - `actionErrorCode`, which an integration widens with its own codes
  - `actionOutcomeLocked` and `actionCompleted`
  - `capabilityReason`, now the one "why is this disabled" wording
- **Utilities:** `utils/drawer-trail.ts` holds the `?drawer=` codec, and
`utils/page-links.ts` the back label and the subject-filtered Issues
link.
- **Kind table:** CNPG's detail kinds derive kind and group from the
workspace kind table.

### Server
- **`internal/server/actions.go`**: the reviewed-action contract.
- `ActionCapability`, `ActionRequest`, `decodeActionRequest` and
`decodeActionParams`
- refusals: `changedAction`, `blockedAction`, `partialAction` and
`writeActionError`
- the permission decision: `grantPermission` and `capabilityVerdict`,
with a real SelfSubjectAccessReview in local mode
  - `mergePatchAtVersion`
- **Grants:** `Grant` lives in `internal/auth` and carries `In(ns)` and
`String()`.
- **Kind access:** `kind_access.go` covers per-kind access and coverage
(`readWorkspaceKind`, `typedKindScope`, `KindCoverage`);
`cache_scope.go` has `namespacesWithinCache`.
- **Other shared helpers:**
- `read_source.go`: one `ReadSource` shape for HA, recovery and storage
  - `fanout.go`: the bounded per-namespace fan-out
- `internal/prometheus/series_scope.go`: `SeriesIsolation`, already used
by the PVC usage charts
- **Kept CNPG-specific:** facts, guards, runners and the extra refusal
codes.
- **Exported manager detection:** `pkg/topology` now exports
`ManagedByFromMeta`. This is additive.

### Wire changes (CNPG endpoints, unreleased)
- **`grant`** is `{verb, group?, resource, subresource?, namespace?}`
(no namespace means cluster-wide) on every capability, read source,
chart and report item. The wording is unchanged and comes from
`Grant.String()` / `formatGrant`.
- **Kind coverage** gains:
  - a state `uncached`, for a scope Radar's cache does not cover at all;
- `uncachedNamespaces`, named under the same rule as `deniedNamespaces`.

Before, a namespace the caller can read but Radar does not cache was
reported as denied, and the UI said "No access". It now says "Radar does
not cache Pods in db".
- **`/api/cnpg/workspace`** gains `managedBy`, keyed `Kind/ns/name`.
- "Declared in" now uses the server's manager detection and links to the
Argo CD application or Flux object.
- Before, the client parsed labels itself. That misnamed Argo CD
applications living outside Argo CD's namespace (`ns_app`) and ignored
the Flux namespace label.

### Version skew
New `FeatureCapabilities` flags: `cnpgWorkspace` (every `/api/cnpg/*`
route except the two image-catalog lookups that predate it) and
`gitopsWriteEvidence`. Each has a `radarFeatures` entry.

Every CNPG query, mutation, log stream, report download and the
write-evidence query goes through `useRadarFeature`. Mutations are never
retried. On a Radar without the workspace:
- the sidebar offers no workspace destinations;
- `/workload` links and drawer Expand stay on the standard views;
- a Cluster's Logs tab shows its Pods' logs;
- a workspace screen opened directly says it needs a newer Radar.

### Vocabulary
On screen these areas are named by their subject. The block above a
sidebar category's kinds reads **Views**, and the copy says
"CloudNativePG views" and "CloudNativePG data", never "workspace",
because Radar Hub uses "workspace" for the customer's account.
"Workspace" remains the internal term for an integration with several
kinds and its own screens (the integration guide,
`SidebarCategoryWorkspace`, `/api/cnpg/workspace`).

### Docs
- **DESIGN.md:** the rules for unknown, partial and denied values are
written once. `capacity.md` and `cnpg.md` link to them and keep their
per-value tables.
- **`docs/INTEGRATION_GUIDE.md`:** a new "Workspace integrations"
chapter covering when a workspace is warranted, the pieces to import,
and what is not shared yet. CLAUDE.md points to it.

## Not shared yet
These have one consumer, and their interface should come from the
second:
- the workspace registry / App wiring
- the operation tracker
- the generic action runner
- the fixed-path `pods/proxy` reader
- the merged log stream
- the report bundle
- operator diagnosis
- a TTL-memo helper

Rollouts' capabilities answer "allowed" in local mode without asking the
apiserver. That is a real bug, but Rollouts returns bare booleans and
hides denied actions, so it needs its own PR.

## Public surface
- **k8s-ui:** exports that exist on main are unchanged. Renamed exports
were all added in #1921/#1922. `CapacityCertainty` is kept.
- **`pkg/`:** one additive export (`topology.ManagedByFromMeta`).
- **`pkg/capacityapi` v1alpha1:** unchanged.
- **radar-app:** surface unchanged.

## Testing
- **Go:** `go build ./...`; `go test ./...` in both modules. The
`cmd/desktop` env test is flaky and passes alone. `gofmt -l` is clean.
- **Frontend:**
  - type checks pass for web and k8s-ui;
  - vitest: k8s-ui 236 files / 4,369 tests, web 182 files / 2,028 tests;
  - eslint: 0 errors, and no new warnings.
- **Live on the kind CloudNativePG demo, full access and a view-only
identity:**
- every CNPG page rendered with structured grants and no "[object
Object]" or "undefined";
- a namespaced Argo CD tracking ID on a demo Cluster showed "Declared
in: Argo CD application argocd/payments", linked;
- with the capability flags removed from `/api/capabilities` (an older
Radar behind Radar Hub), the sidebar workspace, redirects, composed
summaries and actions fell back to the standard views, and `/cnpg`
showed the upgrade note.

Stacked on #1922 (which is stacked on #1921). Merge order: #1921, #1922,
then this PR, before a release ships `/api/cnpg/*`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches CNPG API shapes, RBAC/coverage semantics, and cluster write
paths via the shared action layer; behavior is intended to be equivalent
with clearer grant and cache messaging.
> 
> **Overview**
> Extracts the generic workspace layer that CloudNativePG had under
CNPG-specific names into shared modules, so future integrations (e.g.
Capacity-style screens) import one contract instead of copying.
> 
> **Server:** Adds `internal/auth.Grant` (structured RBAC on the wire),
`internal/server/actions.go` (reviewed `ActionRequest`, capabilities,
409/partial errors, `mergePatchAtVersion`), `cache_scope.go` /
`kind_access` patterns, and `prometheus/series_scope.go` (shared
Prometheus isolation). CNPG handlers now use these; history/PVC denial
fields expose `*Grant` instead of strings. `FeatureCapabilities` adds
`cnpgWorkspace` and `gitopsWriteEvidence`. CNPG workspace coverage gains
`uncached` / `uncachedNamespaces` (distinct from denied) and `managedBy`
from server-side GitOps detection.
> 
> **UI:** New k8s-ui `facts/`, `problems/`, fold sections,
`formatGrant`, and app `components/workspace/` plus `api/actions.ts`.
CNPG is rewired to structured grants and shared action types; secondary
buttons use `.btn-secondary`.
> 
> **Docs:** DESIGN.md documents unknown/partial/denied values once;
INTEGRATION_GUIDE adds a workspace-integrations chapter; CLAUDE.md
points builders at it.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5af6ed1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
# Conflicts:
#	packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 135d8fd. Configure here.

Comment thread packages/k8s-ui/src/components/workload/WorkloadView.tsx
Comment thread web/src/components/cnpg/CNPGProtection.tsx Outdated
…health inference

A CNPG Cluster's instance Pods are its children, not related Pods, so the
Logs tab no longer depends on them. Protection's Destinations column now
uses the same inference as the ObjectStore summary, so recovery windows
left by clusters that no longer use the store don't read as failures.
@nadaverell
nadaverell merged commit a266d31 into main Oct 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants