fix: handle 204 No Content in fetchJSON, add FK-aware delete errors, v2 screenshots

Frontend: fetchJSON now returns empty object on 204 instead of failing
to parse empty body — fixes silent delete failures across all entities.
Added onError callbacks to owner/team delete mutations to surface errors.

Backend: owner and issuer delete handlers return 409 Conflict with
descriptive messages when FK constraints block deletion, instead of
generic 500.

Added 15 v2 dashboard screenshots, updated README screenshot section,
logo asset, page count references (18→full), and QA guide with FK
constraint test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-25 05:03:50 -04:00
parent 3eb4749b4d
commit 0f4a1b268b
25 changed files with 105 additions and 20 deletions
+18 -12
View File
@@ -71,7 +71,7 @@ certctl gives you a single pane of glass for every TLS certificate in your organ
- **Revocation infrastructure** — RFC 5280 revocation with all standard reason codes, DER-encoded X.509 CRL per issuer, embedded OCSP responder, and short-lived certificate exemption (certs under 1 hour skip CRL/OCSP).
- **Policy engine** — 5 rule types with violation tracking and severity levels. Certificate profiles enforce allowed key types, maximum TTL, and crypto constraints at enrollment time.
- **Immutable audit trail** — every action recorded to an append-only log. Every API call recorded with method, path, actor, SHA-256 body hash, response status, and latency. No update or delete on audit records.
- **Operational dashboard** — 18-page React GUI with certificate inventory, bulk operations (multi-select renew/revoke/reassign), deployment timeline visualization, inline policy editing, agent fleet overview, expiration heatmaps, and real-time short-lived credential tracking.
- **Operational dashboard** — Full React GUI with certificate inventory, bulk operations (multi-select renew/revoke/reassign), deployment timeline visualization, inline policy editing, agent fleet overview, expiration heatmaps, and real-time short-lived credential tracking.
- **Observability** — JSON and Prometheus metrics endpoints, 5 stats API endpoints for dashboards, structured slog logging with request ID propagation. Compatible with Prometheus, Grafana Agent, Datadog Agent, and Victoria Metrics.
- **Notifications** — threshold-based alerting with deduplication. Routes to email, webhooks, Slack, Microsoft Teams, PagerDuty, and OpsGenie.
- **AI and CLI access** — MCP server exposes all 78 API operations as tools for Claude, Cursor, and any MCP-compatible client. CLI tool with 12 subcommands for terminal workflows and scripting.
@@ -99,16 +99,22 @@ flowchart LR
| | |
|---|---|
| ![Dashboard](docs/screenshots/dashboard.png) | ![Certificates](docs/screenshots/certificates.png) |
| **Dashboard**certificate stats, expiry timeline, recent jobs | **Certificates** — full inventory with status, environment, owner filters |
| ![Agents](docs/screenshots/agents.png) | ![Jobs](docs/screenshots/jobs.png) |
| **Agents** — fleet health, hostname, heartbeat tracking | **Jobs** — issuance, renewal, deployment job queue |
| ![Notifications](docs/screenshots/notifications.png) | ![Policies](docs/screenshots/policies.png) |
| **Notifications** — threshold alerts grouped by certificate | **Policies** — enforcement rules with enable/disable and delete |
| ![Issuers](docs/screenshots/issuers.png) | ![Targets](docs/screenshots/targets.png) |
| **Issuers**CA connectors with test connectivity | **Targets** — deployment targets (NGINX, Apache, HAProxy, F5, IIS) |
| ![Audit Trail](docs/screenshots/audit-trail.png) | |
| **Audit Trail** — immutable log of every action | |
| ![Dashboard](docs/screenshots/v2/dashboard.png) | ![Certificates](docs/screenshots/v2/certificates.png) |
| **Dashboard**real-time stats, expiration heatmap, renewal trends, issuance rate | **Certificates** — full inventory with status filters, environment, owner, team |
| ![Agents](docs/screenshots/v2/agents.png) | ![Fleet Overview](docs/screenshots/v2/fleet-overview.png) |
| **Agents** — fleet health, hostname, OS/arch, IP, version tracking | **Fleet Overview** — OS distribution, status breakdown, version analysis |
| ![Jobs](docs/screenshots/v2/jobs.png) | ![Notifications](docs/screenshots/v2/notifications.png) |
| **Jobs** — issuance, renewal, deployment job queue with status filters | **Notifications** — expiration warnings, renewal results, unread/all toggle |
| ![Policies](docs/screenshots/v2/policies.png) | ![Profiles](docs/screenshots/v2/profiles.png) |
| **Policies**enforcement rules for ownership, environments, lifetime, renewal | **Profiles** — enrollment templates with key types, max TTL, crypto constraints |
| ![Issuers](docs/screenshots/v2/issuers.png) | ![Targets](docs/screenshots/v2/targets.png) |
| **Issuers** — CA connectors (Local CA, Let's Encrypt, step-ca, DigiCert) | **Targets** — deployment targets (NGINX, F5 BIG-IP, IIS, HAProxy) |
| ![Owners](docs/screenshots/v2/owners.png) | ![Teams](docs/screenshots/v2/teams.png) |
| **Owners** — certificate ownership with email and team assignment | **Teams** — organizational grouping for notification routing |
| ![Agent Groups](docs/screenshots/v2/agent-groups.png) | ![Audit Trail](docs/screenshots/v2/audit-trail.png) |
| **Agent Groups** — dynamic grouping by OS, arch, CIDR, version | **Audit Trail** — immutable log with filters, CSV/JSON export |
| ![Short-Lived](docs/screenshots/v2/short-lived.png) | |
| **Short-Lived Credentials** — ephemeral certs with live TTL countdown | |
## Quick Start
@@ -567,7 +573,7 @@ make docker-clean # Stop + remove volumes
## Roadmap
### V1 (v1.0.0 released)
All nine development milestones (M1M9) are complete. The backend covers the full certificate lifecycle: Local CA and ACME v2 issuers, NGINX/Apache/HAProxy/F5/IIS target connectors, threshold-based expiration alerting, agent-side ECDSA P-256 key generation, API auth with rate limiting, and a React dashboard with 18 pages wired to the real API. The CI pipeline runs build, vet, test with coverage gates (service layer 30%+, handler layer 50%+), frontend type checking, Vitest test suite, and Vite production build on every push. Docker images are published to GitHub Container Registry on every version tag via the release workflow.
All nine development milestones (M1M9) are complete. The backend covers the full certificate lifecycle: Local CA and ACME v2 issuers, NGINX/Apache/HAProxy/F5/IIS target connectors, threshold-based expiration alerting, agent-side ECDSA P-256 key generation, API auth with rate limiting, and a full React dashboard wired to the real API. The CI pipeline runs build, vet, test with coverage gates (service layer 30%+, handler layer 50%+), frontend type checking, Vitest test suite, and Vite production build on every push. Docker images are published to GitHub Container Registry on every version tag via the release workflow.
### V2: Operational Maturity
- **M10: Agent Metadata + Targets** ✅ — agents report OS, architecture, IP, hostname, version via heartbeat; Apache httpd and HAProxy target connectors
+1 -1
View File
@@ -92,7 +92,7 @@ The agent runs two background loops: a heartbeat (every 60 seconds) to signal it
The web dashboard is the primary operational interface for certctl. It is built with Vite + React + TypeScript and uses TanStack Query for server state management (caching, background refetching, optimistic updates).
**Current views (19 pages)**: certificate inventory (list with multi-select bulk operations + "New Certificate" creation modal + detail with deployment status timeline, inline policy/profile editor, version history, deploy, revoke, archive, and trigger renewal actions), agent fleet (list + detail with system info + OS/architecture grouping with charts), job queue (status, retry, cancel, approve/reject), notification inbox (threshold alert grouping, mark-as-read), audit trail (time range, actor, action filters + CSV/JSON export), policy management (rules with enable/disable toggle + delete + violations), issuers (list with test connection + delete), targets (list with 3-step configuration wizard + delete), owners (list with team resolution + delete), teams (list with delete), agent groups (list with dynamic match criteria badges + enable/disable + delete), certificate profiles (list with crypto constraints), short-lived credentials dashboard (TTL countdown, profile filtering, auto-refresh), summary dashboard with charts (expiration heatmap, renewal success rate, status distribution, issuance rate), and login page.
**Current views**: certificate inventory (list with multi-select bulk operations + "New Certificate" creation modal + detail with deployment status timeline, inline policy/profile editor, version history, deploy, revoke, archive, and trigger renewal actions), agent fleet (list + detail with system info + OS/architecture grouping with charts), job queue (status, retry, cancel, approve/reject), notification inbox (threshold alert grouping, mark-as-read), audit trail (time range, actor, action filters + CSV/JSON export), policy management (rules with enable/disable toggle + delete + violations), issuers (list with test connection + delete), targets (list with 3-step configuration wizard + delete), owners (list with team resolution + delete), teams (list with delete), agent groups (list with dynamic match criteria badges + enable/disable + delete), certificate profiles (list with crypto constraints), short-lived credentials dashboard (TTL countdown, profile filtering, auto-refresh), summary dashboard with charts (expiration heatmap, renewal success rate, status distribution, issuance rate), and login page.
The dashboard includes an **ErrorBoundary component** for graceful error recovery — if a view crashes, the boundary catches the error and displays a user-friendly message instead of breaking the entire dashboard. It also includes a **demo mode** that activates when the API is unreachable — it renders realistic mock data for screenshots and offline presentations.
+3 -3
View File
@@ -818,7 +818,7 @@ All loops have configurable intervals via environment variables (`CERTCTL_SCHEDU
---
## Web Dashboard (19 Pages)
## Web Dashboard
### Overview
The web dashboard is the primary operational interface for certctl. Built with **Vite + React 18 + TypeScript + TanStack Query v5 + Tailwind CSS 3 + Recharts**.
@@ -1168,7 +1168,7 @@ Each guide includes an evidence summary table mapping specific criteria to certc
| Policies + violations | ✓ | ✓ | Shipped |
| Profiles + crypto constraints | ✓ | ✓ | Shipped |
| Revocation (RFC 5280, CRL, OCSP) | ✓ | ✓ | Shipped |
| Dashboard + 19 pages | ✓ | ✓ | Shipped |
| Full web dashboard | ✓ | ✓ | Shipped |
| Observability (charts, metrics, stats) | ✓ | ✓ | Shipped |
| REST API (91 endpoints) | ✓ | ✓ | Shipped |
| MCP server (78 tools) | ✓ | ✓ | Shipped v2.1 |
@@ -1200,7 +1200,7 @@ Each guide includes an evidence summary table mapping specific criteria to certc
| Category | Count |
|----------|-------|
| **API Endpoints** | 91 (under /api/v1/) |
| **Dashboard Pages** | 19 |
| **Dashboard** | Full web GUI |
| **Issuer Connectors** | 4 (Local CA, ACME, step-ca, OpenSSL) |
| **Target Connectors** | 5 (3 impl: NGINX, Apache, HAProxy; 2 stubs: F5, IIS) |
| **Notifier Channels** | 6 (Email, Webhook, Slack, Teams, PagerDuty, OpsGenie) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

+66 -2
View File
@@ -2094,6 +2094,49 @@ curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/agent-gr
---
### 11.4 Foreign Key Constraint Behavior
**What this validates:** Delete operations correctly fail with 409 when referenced entities still exist.
**Why it matters:** Owners and issuers use `ON DELETE RESTRICT` — you can't delete them while certificates reference them. Teams use `ON DELETE CASCADE`, so team deletes succeed and cascade. If the server returns a silent 500 instead of 409, the GUI swallows the error and the user thinks nothing happened.
**Test 11.4.1 — Delete owner with assigned certificates (expect 409)**
```bash
# Try to delete Alice Chen (o-alice) — she owns certificates in the demo data
curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/owners/o-alice" | jq .
```
**Expected:** HTTP 409 with message "Cannot delete owner: certificates are still assigned to this owner".
**PASS if** 409 Conflict. **FAIL** if 204 (data integrity violation) or 500 (unhelpful error).
---
**Test 11.4.2 — Delete issuer with assigned certificates (expect 409)**
```bash
# Try to delete the Local Dev CA (iss-local) — certificates reference it
curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/issuers/iss-local" | jq .
```
**Expected:** HTTP 409 with message "Cannot delete issuer: certificates are still using this issuer".
**PASS if** 409 Conflict. **FAIL** if 204 or 500.
---
**Test 11.4.3 — Delete team cascades successfully**
```bash
# Create a test team, then delete it — teams use ON DELETE CASCADE
curl -s -X POST -H "$AUTH" -H "$CT" -d '{"id": "t-fk-test", "name": "FK Test Team"}' $SERVER/api/v1/teams > /dev/null
curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/teams/t-fk-test"
```
**Expected:** HTTP 204 (cascade allows deletion).
**PASS if** 204. **FAIL** if 409 or 500.
---
## Part 12: Notifications
**What this validates:** Notification creation, listing, and read status management.
@@ -3192,7 +3235,7 @@ echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_certificate",
## Part 19: GUI Testing
**What this validates:** The web dashboard — 19 pages of operational UI.
**What this validates:** The full web dashboard — all pages of operational UI.
**Why it matters:** Operators spend 80% of their time in the GUI. If it's broken, the product is broken, regardless of how good the API is.
@@ -3741,7 +3784,28 @@ curl -s -H "$AUTH" "$SERVER/api/v1/network-scan-targets" | jq '{total, ids: [.it
---
**Test 25.1.4 — OpenAPI spec operations match router**
**Test 25.1.4 — GUI delete on FK-restricted entities shows error, not silent failure**
```bash
# Try deleting owner o-alice via API — she owns demo certificates
CODE=$(curl -s -o /tmp/delete-resp.json -w "%{http_code}" -X DELETE -H "$AUTH" "$SERVER/api/v1/owners/o-alice")
echo "DELETE owner with certs: HTTP $CODE"
cat /tmp/delete-resp.json | jq .
# Try deleting issuer iss-local — certificates reference it
CODE=$(curl -s -o /tmp/delete-resp.json -w "%{http_code}" -X DELETE -H "$AUTH" "$SERVER/api/v1/issuers/iss-local")
echo "DELETE issuer with certs: HTTP $CODE"
cat /tmp/delete-resp.json | jq .
```
**What:** Verifies that deleting owners/issuers with assigned certificates returns 409 Conflict with a descriptive message.
**Why:** This was a real bug — the backend returned 500 (generic "Failed to delete"), `fetchJSON` threw on the error, and TanStack Query's `onError` wasn't wired up. The user clicked OK on the confirm dialog and nothing visibly happened. Fixed by: (1) backend returns 409 with descriptive message for FK constraint violations, (2) `fetchJSON` handles 204 No Content for successful deletes, (3) frontend mutation `onError` surfaces the error.
**Expected:** Both return HTTP 409 with descriptive conflict messages.
**PASS if** both 409 with messages. **FAIL** if 500 (unhelpful error) or 204 (data integrity violation).
---
**Test 25.1.5 — OpenAPI spec operations match router**
```bash
echo "OpenAPI operations: $(grep -c 'operationId:' api/openapi.yaml)"
+6
View File
@@ -184,7 +184,13 @@ func (h IssuerHandler) DeleteIssuer(w http.ResponseWriter, r *http.Request) {
}
if err := h.svc.DeleteIssuer(id); err != nil {
if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") {
ErrorWithRequestID(w, http.StatusConflict, "Cannot delete issuer: certificates are still using this issuer", requestID)
} else if strings.Contains(err.Error(), "not found") {
ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID)
} else {
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID)
}
return
}
+6
View File
@@ -183,7 +183,13 @@ func (h OwnerHandler) DeleteOwner(w http.ResponseWriter, r *http.Request) {
id = parts[0]
if err := h.svc.DeleteOwner(id); err != nil {
if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") {
ErrorWithRequestID(w, http.StatusConflict, "Cannot delete owner: certificates are still assigned to this owner", requestID)
} else if strings.Contains(err.Error(), "not found") {
ErrorWithRequestID(w, http.StatusNotFound, "Owner not found", requestID)
} else {
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID)
}
return
}
+1
View File
@@ -42,6 +42,7 @@ async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
}
throw new Error(errorMsg || `HTTP ${res.status}`);
}
if (res.status === 204) return {} as T;
return res.json();
}
+1
View File
@@ -23,6 +23,7 @@ export default function OwnersPage() {
const deleteMutation = useMutation({
mutationFn: deleteOwner,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['owners'] }),
onError: (err: Error) => alert(`Delete failed: ${err.message}`),
});
const teamMap = new Map<string, Team>();
+1
View File
@@ -18,6 +18,7 @@ export default function TeamsPage() {
const deleteMutation = useMutation({
mutationFn: deleteTeam,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['teams'] }),
onError: (err: Error) => alert(`Delete failed: ${err.message}`),
});
const columns: Column<Team>[] = [