From 29533777fb0cda7cbc98b6e66f8b7321bd125c96 Mon Sep 17 00:00:00 2001 From: certctl-bot Date: Sat, 25 Apr 2026 15:23:15 +0000 Subject: [PATCH] fix(web,ci): close orphan-CRUD GUI gaps + dead exportCertificatePEM (B-1 master) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes four 2026-04-24 audit findings via per-page Edit modals on five existing pages, a brand-new RenewalPoliciesPage for the rp-* CRUD surface, and removal of one dead duplicate so the public client surface stops growing without consumers. Anchored by a CI grep guardrail that fails the build if any of the eight previously-orphan client functions loses its non-test page consumer or if exportCertificatePEM is resurrected. Per-page Edit modals (mirroring existing CreateXModal scaffolding): - web/src/pages/OwnersPage.tsx — EditOwnerModal (name/email/team_id) - web/src/pages/TeamsPage.tsx — EditTeamModal (name/description) - web/src/pages/AgentGroupsPage.tsx — EditAgentGroupModal (full match-rule set: name/description/match_os/match_architecture/match_ip_cidr/ match_version/enabled) - web/src/pages/IssuersPage.tsx — EditIssuerModal (rename-only; type locked, config blob preserved untouched, footer note about delete+ recreate for credential rotation) - web/src/pages/ProfilesPage.tsx — EditProfileModal (rename + description only; policy fields preserved untouched, footer note about deferred policy editing) New page (closes cat-b-4631ca092bee — RenewalPolicy CRUD orphan): - web/src/pages/RenewalPoliciesPage.tsx — full CRUD page with shared PolicyFormModal for Create + Edit (form shape identical), 7-column DataTable (Policy/RenewalWindow/Auto/Retries/AlertThresholds/Created/ Actions), comma-separated alert_thresholds_days input parser, and alert() surfacing of repository.ErrRenewalPolicyInUse (409) on Delete so operators can re-target dependent certs before deletion. - web/src/main.tsx — adds /renewal-policies route. - web/src/components/Layout.tsx — adds sidebar nav item slotted between Policies and Profiles. Removed (closes cat-b-9b97ffb35ef7 — dead duplicate): - web/src/api/client.ts::exportCertificatePEM — zero consumers across web/, MCP, CLI, tests; downloadCertificatePEM is the actual call site in CertificateDetailPage. Test references in client.test.ts and client.error.test.ts also removed. CI regression guardrail: - .github/workflows/ci.yml — adds 'Forbidden orphan-CRUD client function regression guard (B-1)' step. Greps for all eight previously-orphan fns (updateOwner/updateTeam/updateAgentGroup/updateIssuer/updateProfile + createRenewalPolicy/updateRenewalPolicy/deleteRenewalPolicy) under web/src/pages/ and fails the build if any has zero non-test consumers. Also blocks resurrection of exportCertificatePEM. Verified locally (all 8 fns have ≥2 consumers; exportCertificatePEM is gone) and against synthetic regressions. Documentation: - CHANGELOG.md — new B-1 section above L-1 under [unreleased]. - docs/architecture.md — Web Dashboard section gains a new paragraph capturing the 'every backend CRUD must have a GUI consumer' rule with reference to the CI guardrail. - coverage-gap-audit-2026-04-24-v5/unified-audit.md — flips four findings to ✅ RESOLVED with detailed Status blocks; bumps Live Tracker score 16/47 → 20/47 (P1: 9→12, P3: 1→2); adds B-1 row to closed-bundle index. Verification: - cd web && tsc --noEmit — clean - cd web && vitest run — 9 test files, 294 tests, all passing - cd web && vite build — clean (no new warnings) - B-1 guardrail dry-run — all 8 client fns have ≥2 page consumers, exportCertificatePEM removed (good), FAIL=0 Audit findings closed: - cat-b-31ceb6aaa9f1 (P1, updateOwner/updateTeam/updateAgentGroup orphan) - cat-b-7a34f893a8f9 (P1, updateIssuer/updateProfile orphan, rename-only) - cat-b-4631ca092bee (P1, RenewalPolicy CRUD orphan) - cat-b-9b97ffb35ef7 (P3, exportCertificatePEM dead duplicate) Deferred follow-ups: - Fuller EditIssuerModal with credential-rotation flow (needs threat model: rotation reuse window, in-flight CSR cancellation, audit-trail granularity). - Fuller EditProfileModal with policy-field editing (max-TTL, allowed EKUs, allowed key algorithms — affect already-issued cert evaluation). - Per-page Vitest coverage for the new Edit modals (CI grep guardrail catches the same regression vector at lower cost). --- .github/workflows/ci.yml | 52 ++++ CHANGELOG.md | 33 +++ docs/architecture.md | 2 + web/src/api/client.error.test.ts | 7 +- web/src/api/client.test.ts | 12 +- web/src/api/client.ts | 11 +- web/src/components/Layout.tsx | 1 + web/src/main.tsx | 2 + web/src/pages/AgentGroupsPage.tsx | 148 +++++++++++- web/src/pages/IssuersPage.tsx | 116 ++++++++- web/src/pages/OwnersPage.tsx | 137 ++++++++++- web/src/pages/ProfilesPage.tsx | 127 +++++++++- web/src/pages/RenewalPoliciesPage.tsx | 327 ++++++++++++++++++++++++++ web/src/pages/TeamsPage.tsx | 103 +++++++- 14 files changed, 1026 insertions(+), 52 deletions(-) create mode 100644 web/src/pages/RenewalPoliciesPage.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314cfd8..a59745d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -388,6 +388,58 @@ jobs: exit 1 fi + - name: Forbidden orphan-CRUD client function regression guard (B-1) + # B-1 master closed four audit findings — three orphan-update fns + # (cat-b-31ceb6aaa9f1, cat-b-7a34f893a8f9) and one orphan CRUD + # surface (cat-b-4631ca092bee, RenewalPolicy) — by wiring per-page + # Edit modals so every backend write endpoint has at least one + # GUI consumer. The fourth finding (cat-b-9b97ffb35ef7) deleted + # the dead `exportCertificatePEM` duplicate. + # + # Pre-B-1 the failure mode was: backend ships a CRUD handler, + # client.ts ships the matching `update*` / `delete*` / `create*` + # function, but no page imports it. Operators were forced to + # `psql` directly to edit team names, owner emails, agent-group + # match rules, issuer names, profile names, or any renewal-policy + # field — turning a 30-second GUI task into a 30-minute database + # excursion with audit-trail gaps. + # + # This step fails the build if any of the eight previously-orphan + # client functions loses its page consumer (i.e. a future refactor + # accidentally re-orphans them). Each fn must have ≥1 non-test + # consumer under web/src/pages/. Tests (*.test.ts(x)) and the + # client.ts definition file itself are exempt. + # + # See coverage-gap-audit-2026-04-24-v5/unified-audit.md + # cat-b-31ceb6aaa9f1, cat-b-7a34f893a8f9, cat-b-4631ca092bee, + # cat-b-9b97ffb35ef7 for closure rationale. + run: | + set -e + ORPHAN_FNS="updateOwner updateTeam updateAgentGroup updateIssuer updateProfile createRenewalPolicy updateRenewalPolicy deleteRenewalPolicy" + FAIL=0 + for fn in $ORPHAN_FNS; do + HITS=$(grep -rE "\b${fn}\b" web/src/pages/ 2>/dev/null \ + | grep -vE '\.test\.(ts|tsx):' \ + | wc -l) + if [ "$HITS" -eq 0 ]; then + echo "::error::B-1 regression: client function '${fn}' has zero consumers under web/src/pages/." + echo " Every backend CRUD endpoint must have a GUI consumer to avoid forcing operators to psql." + echo " Either restore the page consumer or delete the client function in the same commit." + FAIL=1 + fi + done + # cat-b-9b97ffb35ef7: exportCertificatePEM was deleted as a dead + # duplicate of downloadCertificatePEM. Block resurrection. + if grep -nE 'export\s+const\s+exportCertificatePEM' web/src/api/client.ts >/dev/null 2>&1; then + echo "::error::B-1 regression: exportCertificatePEM was removed as a dead duplicate of downloadCertificatePEM." + echo " If a JSON variant is needed, add an explicit page consumer in the same commit." + FAIL=1 + fi + if [ "$FAIL" -ne 0 ]; then + exit 1 + fi + echo "B-1 orphan-CRUD client function guardrail: all 8 functions have page consumers." + - name: Race Detection run: go test -race ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/scheduler/... ./internal/connector/... ./internal/crypto/... ./internal/domain/... ./internal/validation/... ./internal/tlsprobe/... -count=1 -timeout 300s diff --git a/CHANGELOG.md b/CHANGELOG.md index 468ee2b..b51ee1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to certctl are documented in this file. Dates use ISO 8601. ## [unreleased] — 2026-04-25 +### B-1: Orphan-CRUD client functions + RenewalPolicy GUI gap — closed end-to-end + +> The 2026-04-24 coverage-gap audit flagged a cluster of operator-blocking GUI omissions: six client.ts `update*` functions (`updateOwner`, `updateTeam`, `updateAgentGroup`, `updateIssuer`, `updateProfile`, plus the full `*RenewalPolicy` CRUD trio) had backend handlers, OpenAPI operations, and exported TypeScript fetchers — but zero page consumers. Operators wanting to fix a typo in an owner's email, rename a team, retarget an agent group's match rules, or edit a renewal-policy field were forced to either delete-and-recreate (losing FK history and audit-trail continuity) or open a `psql` session against the production database directly. The audit's blunt summary: "every backend feature ships with its GUI surface" — a load-bearing CLAUDE.md invariant — was being violated for five operator-facing entities. B-1 closes that violation by wiring per-page Edit modals onto five existing pages, adding a brand-new `RenewalPoliciesPage` for the rp-* CRUD surface, and deleting one dead duplicate (`exportCertificatePEM`) so the public client surface area stops growing without consumers. + +### Breaking Changes + +None. All five existing pages keep their Create + Delete affordances unchanged; Edit is purely additive. `RenewalPoliciesPage` is a new route at `/renewal-policies` and a new sidebar nav item slotted between Policies and Profiles. The `exportCertificatePEM` helper had zero consumers in `web/`, MCP, CLI, and tests at the time of removal — operators using `downloadCertificatePEM` (the actual call site in `CertificateDetailPage`) are unaffected. + +### Added + +- **`web/src/pages/RenewalPoliciesPage.tsx`** — a new full-CRUD page for the `rp-*` renewal-policy table. Surfaces a 7-column DataTable (Policy / Renewal Window / Auto / Retries / Alert Thresholds / Created / Actions) with Create, Edit, and Delete affordances. A shared `PolicyFormModal` powers both Create and Edit (the form shape is identical) covering the full domain field set: `name`, `renewal_window_days`, `auto_renew`, `max_retries`, `retry_interval_seconds`, `alert_thresholds_days[]`. The thresholds input parses comma-separated integers (`30, 14, 7, 0`) into the array shape the backend expects. Delete surfaces `repository.ErrRenewalPolicyInUse` (409 from the backend when a policy still has `managed_certificates.renewal_policy_id` references) via an explicit alert so the operator can re-target the dependent certs to a different policy before deletion. Wired into `web/src/main.tsx` routing and `web/src/components/Layout.tsx` sidebar nav. +- **EditOwnerModal** in `web/src/pages/OwnersPage.tsx` — pre-populates from the editing owner via `useEffect`, calls `updateOwner(id, {name, email, team_id})`, mirrors the Create modal's TanStack-Query mutation/invalidation pattern. +- **EditTeamModal** in `web/src/pages/TeamsPage.tsx` — same shape, fields `name`/`description`. +- **EditAgentGroupModal** in `web/src/pages/AgentGroupsPage.tsx` — covers the full match-rule set (`name`, `description`, `match_os`, `match_architecture`, `match_ip_cidr`, `match_version`, `enabled`). +- **EditIssuerModal** in `web/src/pages/IssuersPage.tsx` — deliberately rename-only. The `type` field is shown but disabled, the existing `config` blob (which includes credentials for ACME, ADCS, ZeroSSL, etc.) is forwarded untouched, and only `name` is editable. Footer note: "To change issuer type or rotate credentials, delete and recreate." This trades scope for safety — the audit's destructive-rename complaint is closed without surfacing a credential-edit attack surface that has not been threat-modeled. +- **EditProfileModal** in `web/src/pages/ProfilesPage.tsx` — same rename-only shape. Forwards full `Partial` with policy fields (`allowed_key_algorithms`, `max_ttl_seconds`, `allowed_ekus`, etc.) preserved untouched. Footer note about deferred policy-field editing. +- **CI regression guardrail** in `.github/workflows/ci.yml` (`Forbidden orphan-CRUD client function regression guard (B-1)`) — grep-fails the build if any of the eight previously-orphan client functions (`updateOwner`, `updateTeam`, `updateAgentGroup`, `updateIssuer`, `updateProfile`, `createRenewalPolicy`, `updateRenewalPolicy`, `deleteRenewalPolicy`) loses its non-test consumer under `web/src/pages/`. Also blocks resurrection of the deleted `exportCertificatePEM` function. Verified locally on the post-fix tree (passes — all 8 fns have ≥2 consumers); fires against synthetic regressions (delete the Edit modal → guardrail fires the next CI run). + +### Removed + +- `web/src/api/client.ts::exportCertificatePEM` — closes `cat-b-9b97ffb35ef7`. The function returned `{cert_pem, chain_pem, full_pem}` JSON but had zero consumers across `web/`, MCP, CLI, and tests; `downloadCertificatePEM` (the blob-download path consumed by `CertificateDetailPage`) covers all real call sites. Test references in `web/src/api/client.test.ts` and `client.error.test.ts` were also removed. The CI guardrail blocks resurrection without an accompanying page consumer. + +### Audit findings closed + +- `cat-b-31ceb6aaa9f1` (P1, `updateOwner`/`updateTeam`/`updateAgentGroup` orphan) +- `cat-b-7a34f893a8f9` (P1, `updateIssuer`/`updateProfile` orphan, rename-only closure) +- `cat-b-4631ca092bee` (P1, RenewalPolicy CRUD orphan — new RenewalPoliciesPage) +- `cat-b-9b97ffb35ef7` (P3, `exportCertificatePEM` dead duplicate) + +### Known follow-ups (deferred from B-1 scope) + +A fuller `EditIssuerModal` with explicit credential-rotation flow is deferred — that needs an explicit threat model (rotation reuse window, audit-trail granularity, in-flight CSR cancellation), and the audit's destructive-rename complaint is closed by rename-only Edit alone. Likewise an `EditProfileModal` with policy-field editing (max-TTL, allowed EKUs, allowed key algorithms) is deferred because policy edits affect the `enforce_certificate_policy` evaluator's semantics for already-issued certs and warrant their own scope. Per-page Vitest coverage for the new Edit modals is deferred — the CI grep guardrail catches the same regression vector ("page lost its `update*` fn consumer") at lower cost than five new test files. + ### L-1: Client-side bulk-action loops — closed end-to-end > The certctl dashboard's busiest screen (`CertificatesPage.tsx`) had two bulk-action workflows that looped per-cert HTTP calls. Selecting 100 certs and clicking "Renew" issued 100 sequential `POST /api/v1/certificates/{id}/renew` requests; "Reassign owner" issued 100 sequential `PUT /api/v1/certificates/{id}` requests. Each round-trip carried ~50–200 ms of Auth → audit-log → handler → service → repo → DB → audit-write → response, so a 100-cert bulk action was a 5–20-second wedge during which the operator stared at a progress bar. The bulk-revoke endpoint (`POST /api/v1/certificates/bulk-revoke`) already shipped in v2.0.x as the canonical pattern for this; L-1 ports that exact shape to bulk-renew (P1) and bulk-reassign (P2). One backend round-trip; one audit event for the entire operation; per-cert success/skip/error counts in a single response envelope. Bundled with two new MCP tools and an OpenAPI spec update so non-GUI callers (CLI / MCP / blackbox probes) can use the same endpoints. diff --git a/docs/architecture.md b/docs/architecture.md index 3d09cfe..2ddc408 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -163,6 +163,8 @@ The dashboard includes an **ErrorBoundary component** for graceful error recover - Light content area with branded dark teal sidebar, Inter + JetBrains Mono typography - SSE/WebSocket planned for real-time job status updates +**Backend ↔ frontend round-trip rule (B-1 closure):** every backend CRUD operation must have at least one GUI consumer in `web/src/pages/`. Shipping a handler + repository method + OpenAPI operation + `client.ts` fetcher with no page that calls it leaves operators forced to `psql` directly — defeats the "every backend feature ships with its GUI surface" invariant and creates a destructive workflow when the missing path is `update*` (operators delete-and-recreate, losing FK history and audit-trail continuity). The CI guardrail in `.github/workflows/ci.yml` (`Forbidden orphan-CRUD client function regression guard (B-1)`) enforces this for the eight previously-orphan functions (`updateOwner`/`updateTeam`/`updateAgentGroup`/`updateIssuer`/`updateProfile` + `createRenewalPolicy`/`updateRenewalPolicy`/`deleteRenewalPolicy`); apply the same rule when adding any new write endpoint. If a fetcher is needed in `client.ts` before its consumer page exists, leave a TODO referencing this rule and ship them in the same commit. + ### PostgreSQL Database All state is stored in PostgreSQL 16. The schema uses TEXT primary keys (not UUIDs) with human-readable prefixed IDs like `mc-api-prod`, `t-platform`, `o-alice`. diff --git a/web/src/api/client.error.test.ts b/web/src/api/client.error.test.ts index 315a178..ecfc999 100644 --- a/web/src/api/client.error.test.ts +++ b/web/src/api/client.error.test.ts @@ -6,7 +6,6 @@ import { createCertificate, triggerRenewal, revokeCertificate, - exportCertificatePEM, downloadCertificatePEM, exportCertificatePKCS12, getAgents, @@ -106,10 +105,8 @@ describe('API Client - Error Handling', () => { ); }); - it('exportCertificatePEM propagates network error', async () => { - mockFetch.mockReturnValueOnce(mockNetworkError()); - await expect(exportCertificatePEM('mc-test')).rejects.toThrow('Failed to fetch'); - }); + // B-1 closure (cat-b-9b97ffb35ef7): exportCertificatePEM removed as a + // dead duplicate of downloadCertificatePEM (zero consumers). it('downloadCertificatePEM propagates network error', async () => { mockFetch.mockReturnValueOnce(mockNetworkError()); diff --git a/web/src/api/client.test.ts b/web/src/api/client.test.ts index 8ece8b0..6cbdc9d 100644 --- a/web/src/api/client.test.ts +++ b/web/src/api/client.test.ts @@ -13,7 +13,6 @@ import { archiveCertificate, revokeCertificate, bulkRevokeCertificates, - exportCertificatePEM, downloadCertificatePEM, exportCertificatePKCS12, getAgents, @@ -1151,15 +1150,8 @@ describe('API Client', () => { // ─── Certificate Export ──────────────────────────────── describe('Certificate Export', () => { - it('exportCertificatePEM fetches PEM data as JSON', async () => { - const pemResult = { cert_pem: 'CERT', chain_pem: 'CHAIN', full_pem: 'FULL' }; - mockFetch.mockReturnValueOnce(mockJsonResponse(pemResult)); - const result = await exportCertificatePEM('mc-1'); - const [url] = mockFetch.mock.calls[0]; - expect(url).toBe('/api/v1/certificates/mc-1/export/pem'); - expect(result.cert_pem).toBe('CERT'); - expect(result.full_pem).toBe('FULL'); - }); + // B-1 closure (cat-b-9b97ffb35ef7): exportCertificatePEM was removed + // from client.ts as a dead duplicate of downloadCertificatePEM. it('downloadCertificatePEM fetches blob with download=true', async () => { const mockBlob = new Blob(['pem-data'], { type: 'application/x-pem-file' }); diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 2e5a62e..fefe97a 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -188,9 +188,14 @@ export const bulkReassignCertificates = (request: BulkReassignmentRequest) => }); // Certificate Export -export const exportCertificatePEM = (id: string) => - fetchJSON<{ cert_pem: string; chain_pem: string; full_pem: string }>(`${BASE}/certificates/${id}/export/pem`); - +// +// B-1 master closure (cat-b-9b97ffb35ef7): the previous `exportCertificatePEM` +// helper that returned `{cert_pem, chain_pem, full_pem}` JSON was removed — +// it had zero consumers across web/, MCP, CLI, and tests, and was a dead +// duplicate of `downloadCertificatePEM` which is the only call site that +// actually exists in `CertificateDetailPage` (browser file-download path). +// If a JSON variant is ever needed again, re-add an explicit fetcher with a +// page consumer in the same commit; do not resurrect the orphan. export const downloadCertificatePEM = (id: string) => { const headers: Record = {}; if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index b789e1f..4f81324 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -10,6 +10,7 @@ const nav = [ { to: '/jobs', label: 'Jobs', icon: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' }, { to: '/notifications', label: 'Notifications', icon: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9' }, { to: '/policies', label: 'Policies', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4' }, + { to: '/renewal-policies', label: 'Renewal Policies', icon: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' }, { to: '/profiles', label: 'Profiles', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' }, { to: '/issuers', label: 'Issuers', icon: 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' }, { to: '/targets', label: 'Targets', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }, diff --git a/web/src/main.tsx b/web/src/main.tsx index 050535b..cc26bbc 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -14,6 +14,7 @@ import AgentDetailPage from './pages/AgentDetailPage'; import JobsPage from './pages/JobsPage'; import NotificationsPage from './pages/NotificationsPage'; import PoliciesPage from './pages/PoliciesPage'; +import RenewalPoliciesPage from './pages/RenewalPoliciesPage'; import IssuersPage from './pages/IssuersPage'; import TargetsPage from './pages/TargetsPage'; import ProfilesPage from './pages/ProfilesPage'; @@ -62,6 +63,7 @@ createRoot(document.getElementById('root')!).render( } /> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/pages/AgentGroupsPage.tsx b/web/src/pages/AgentGroupsPage.tsx index 350b03c..77eecf4 100644 --- a/web/src/pages/AgentGroupsPage.tsx +++ b/web/src/pages/AgentGroupsPage.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { getAgentGroups, deleteAgentGroup, createAgentGroup } from '../api/client'; +import { getAgentGroups, deleteAgentGroup, createAgentGroup, updateAgentGroup } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; @@ -144,9 +144,115 @@ function CreateAgentGroupModal({ isOpen, onClose, onSuccess, isLoading, error }: ); } +// EditAgentGroupModal — B-1 master closure (cat-b-31ceb6aaa9f1). +// Mirrors CreateAgentGroupModal; pre-populates from the editing group; +// calls updateAgentGroup(id, fields) to close the destructive-rename +// hazard. Membership-rule fields (match_os, match_architecture, +// match_ip_cidr, match_version) are editable like the rest — operators +// frequently want to widen/narrow group membership without recreating. +interface EditAgentGroupModalProps { + group: AgentGroup | null; + onClose: () => void; + onSuccess: () => void; + isLoading: boolean; + error: string | null; +} + +function EditAgentGroupModal({ group, onClose, onSuccess, isLoading, error }: EditAgentGroupModalProps) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [matchOs, setMatchOs] = useState(''); + const [matchArch, setMatchArch] = useState(''); + const [matchIpCidr, setMatchIpCidr] = useState(''); + const [matchVersion, setMatchVersion] = useState(''); + const [enabled, setEnabled] = useState(true); + + useEffect(() => { + if (group) { + setName(group.name); + setDescription(group.description || ''); + setMatchOs(group.match_os || ''); + setMatchArch(group.match_architecture || ''); + setMatchIpCidr(group.match_ip_cidr || ''); + setMatchVersion(group.match_version || ''); + setEnabled(group.enabled); + } + }, [group]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!group || !name.trim()) return; + await updateAgentGroup(group.id, { + name: name.trim(), + description: description.trim(), + match_os: matchOs.trim(), + match_architecture: matchArch.trim(), + match_ip_cidr: matchIpCidr.trim(), + match_version: matchVersion.trim(), + enabled, + }); + onSuccess(); + }; + + if (!group) return null; + + return ( +
+
e.stopPropagation()}> +

Edit Agent Group

+

{group.id}

+ {error &&
{error}
} +
+
+ + setName(e.target.value)} required + className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" /> +
+
+ +