` has `role="status"` and `aria-live="polite"` so assistive
- technologies announce selection count changes.
-- `BulkDeleteCertificateDialog` inherits focus trap and `role="dialog"` from Radix `Dialog`.
-- Scrollable cert list inside the dialog has `tabIndex={0}` and `aria-label`.
-- `colSpan` updated 6 → 7 to keep the empty-state row spanning all columns correctly.
-
----
-
-## 4. Implementation Plan
-
-### Phase 1 — New Dialog Component
-
-**File to create**: `frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx`
-
-This phase has no state, no side effects, and no dependencies on Phase 2 or 3. It can be
-reviewed in isolation.
-
-1. Scaffold per §3.5: imports, interface, `providerLabel` helper, component body.
-2. Import `Dialog`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`,
- `DialogFooter` from `../ui/Dialog`.
-3. Import `Button` from `../ui/Button`.
-4. Import `AlertTriangle` from `lucide-react`.
-5. Import `useTranslation` from `react-i18next`.
-6. Import `Certificate` type from `../../api/certificates`.
-7. Export as default.
-
-**Acceptance**: `npx tsc --noEmit` reports 0 errors; component renders without crashing
-in a unit test harness.
-
-### Phase 2 — i18n Keys
-
-**Files to modify** (all under `frontend/src/locales/`):
-`en/translation.json`, `de/translation.json`, `es/translation.json`,
-`fr/translation.json`, `zh/translation.json`
-
-Add the 10 keys from §3.6 to the `"certificates"` object in each file.
-
-**Acceptance**: No missing-key warnings in the browser console or `i18next` debug output
-when the Certificates page is loaded.
-
-### Phase 3 — CertificateList Selection Layer
-
-**File to modify**: `frontend/src/components/CertificateList.tsx`
-
-Step-by-step surgical additions:
-
-1. Add imports: `Checkbox` from `./ui/Checkbox`; `BulkDeleteCertificateDialog` from
- `./dialogs/BulkDeleteCertificateDialog`.
-2. Add state: `selectedIds`, `showBulkDeleteDialog` (§3.1).
-3. Add memos: `selectableCertIds`, `allSelectableSelected`, `someSelected` (§3.1).
-4. Add handlers: `handleSelectAll`, `handleSelectRow` (§3.1).
-5. Add `bulkDeleteMutation` (§3.4).
-6. Extend `ConfigReloadOverlay` condition to `|| bulkDeleteMutation.isPending` (§3.4).
-7. Insert bulk action toolbar above the `
` (§3.3).
-8. Insert leftmost `
` (§3.2.1); update empty-state `colSpan` 6 → 7.
-9. Insert leftmost ` | ` for each row with the three cases A/B/C (§3.2.2).
-10. Mount `` at the end of the fragment (§3.4).
-
-**Invariant**: `isDeletable` and `isInUse` exported function signatures must not change.
-All pre-existing assertions in `CertificateList.test.tsx` must continue to pass.
-
-### Phase 4 — Unit Tests
-
-#### 4.1 New file: `BulkDeleteCertificateDialog.test.tsx`
-
-**File**: `frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx`
-
-Mock `react-i18next` using the `t: (key, opts?) => opts ? JSON.stringify(opts) : key`
-pattern used throughout the test suite.
-
-| # | Test description |
-|---|---|
-| 1 | renders dialog with count in title when 3 certs supplied |
-| 2 | lists each certificate name in the scrollable list |
-| 3 | calls `onConfirm` when the Delete button is clicked |
-| 4 | calls `onCancel` when the Cancel button is clicked |
-| 5 | Delete button is loading/disabled when `isDeleting={true}` |
-| 6 | returns null when `certificates` array is empty |
-
-#### 4.2 Additions to `CertificateList.test.tsx`
-
-Extend the existing `describe('CertificateList', ...)` in
-`frontend/src/components/__tests__/CertificateList.test.tsx`.
-
-The existing fixture (`createCertificatesValue`) already supplies:
-- `id: 1` custom expired, not in use → `isDeletable = true` → enabled checkbox
-- `id: 2` letsencrypt-staging, not in use → `isDeletable = true` → enabled checkbox
-- `id: 4` custom valid, not in use → `isDeletable = true` → enabled checkbox
-- `id: 5` expired LE, not in use → `isDeletable = true` → enabled checkbox
-- `id: 3` custom valid, in use (host has `certificate_id: 3`) → disabled checkbox
-- `id: 6` valid LE, not in use → `isDeletable = false` → no checkbox
-
-| # | Test description |
-|---|---|
-| 1 | renders enabled checkboxes for ids 1, 2, 4, 5 (deletable, not in use) |
-| 2 | renders disabled checkbox (with `aria-disabled`) for id 3 (in-use) |
-| 3 | renders no checkbox in id 6's row (valid production LE) |
-| 4 | selecting one cert makes the bulk action toolbar visible |
-| 5 | header select-all selects only ids 1, 2, 4, 5 — not id 3 (in-use) |
-| 6 | clicking the toolbar Delete button opens `BulkDeleteCertificateDialog` |
-| 7 | confirming in the bulk dialog calls `deleteCertificate` for each selected ID |
-
-### Phase 5 — Playwright E2E Tests
-
-**File to create**: `tests/certificate-bulk-delete.spec.ts`
-
-Reuse `createCustomCertViaAPI` from `tests/certificate-delete.spec.ts`. Import shared
-test helpers from:
-- `tests/fixtures/auth-fixtures` — `test`, `expect`, `loginUser`
-- `tests/utils/wait-helpers` — `waitForLoadingComplete`, `waitForDialog`,
- `waitForAPIResponse`
-- `tests/fixtures/test-data` — `generateUniqueId`
-- `tests/constants` — `STORAGE_STATE`
-
-Seed three custom certs via `beforeAll`, clean up with `afterAll`. Each `test.beforeEach`
-navigates to `/certificates` and calls `waitForLoadingComplete`.
-
-| # | Test scenario |
-|---|---|
-| 1 | **Checkbox column present**: checkboxes appear for each deletable cert |
-| 2 | **No checkbox for valid LE**: valid production LE cert row has no checkbox |
-| 3 | **Select one → toolbar appears**: checking one cert shows the count and Delete button |
-| 4 | **Select-all**: header checkbox selects all three seeded certs; toolbar shows count 3 |
-| 5 | **Dialog shows correct count**: opening bulk dialog shows "Delete 3 Certificate(s)" |
-| 6 | **Cancel preserves certs**: cancelling the dialog leaves all three certs in the list |
-| 7 | **Confirm deletes all selected**: confirming removes all selected certs from the table |
-
-The "confirm deletes" test awaits the success or failure toast appearance (toast appearance
-confirms all requests have settled via `Promise.allSettled`) before asserting the cert
-names are no longer visible in the table.
-
----
-
-## 5. Backend Considerations
-
-### 5.1 No New Endpoint
-
-The existing `DELETE /api/v1/certificates/:id` route at
-`backend/internal/api/routes/routes.go:673` is the only backend touch point. Bulk deletion
-is orchestrated entirely in the frontend using `Promise.allSettled`. This is intentional:
-
-- The volume of certificates eligible for bulk deletion is small in practice.
-- Each deletion independently creates a server-side backup. A batch endpoint would need
- N individual backups anyway, yielding no efficiency gain.
-- Concurrent `Promise.allSettled` provides natural per-item error isolation — a 409 on
- one cert (race: cert becomes in-use between checkbox selection and confirmation) surfaces
- as a failed count in the toast rather than an unhandled rejection.
-
-### 5.2 No Backend Tests Required
-
-The handler tests added in the single-delete PR already cover: success, in-use 409, auth
-guard, invalid ID, not-found, and backup-failure paths. Bulk deletion calls the same handler
-N times with no new code paths. Nothing new at the backend layer warrants new tests.
-
----
-
-## 6. Security Considerations
-
-- Bulk delete inherits all security properties of the individual delete endpoint:
- authentication required, in-use guard server-side, numeric ID validation in the handler.
-- The client-side `isDeletable` check is a UX gate, not a security gate; the server is the
- authoritative enforcer.
-- `Promise.allSettled` does not short-circuit — a 409 on one cert becomes a failed count,
- not an unhandled exception, preserving the remaining deletions.
-
----
-
-## 7. Commit Slicing Strategy
-
-**Single PR.** The entire feature — `BulkDeleteCertificateDialog`, i18n keys in 5 locales,
-selection layer in `CertificateList.tsx`, unit tests, and E2E tests — is one cohesive
-change. The diff is small (< 400 lines of production code across ~6 files) and all parts
-are interdependent. Splitting would temporarily ship a broken feature mid-PR.
-
-Suggested commit title:
-```
-feat(certificates): add bulk deletion with checkbox selection
+"noteText": "You can delete custom certificates, staging certificates, and expired or expiring production certificates that are not attached to any proxy host. Active production certificates are automatically renewed by Caddy.",
```
---
-## 8. File Change Summary
+## 3. Unit Test Updates
-| File | Change | Description |
-|------|--------|-------------|
-| `frontend/src/components/CertificateList.tsx` | Modified | Selection state, checkbox column, bulk toolbar, bulk mutation |
-| `frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx` | Created | New dialog for bulk confirmation |
-| `frontend/src/locales/en/translation.json` | Modified | 10 new i18n keys under `certificates` |
-| `frontend/src/locales/de/translation.json` | Modified | 10 new i18n keys (DE) |
-| `frontend/src/locales/es/translation.json` | Modified | 10 new i18n keys (ES) |
-| `frontend/src/locales/fr/translation.json` | Modified | 10 new i18n keys (FR) |
-| `frontend/src/locales/zh/translation.json` | Modified | 10 new i18n keys (ZH) |
-| `frontend/src/components/__tests__/CertificateList.test.tsx` | Modified | 7 new unit test cases |
-| `frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx` | Created | 6 unit test cases for the new dialog |
-| `tests/certificate-bulk-delete.spec.ts` | Created | E2E test suite (7 scenarios) |
+### 3a. `frontend/src/components/__tests__/CertificateList.test.tsx`
-**Dependencies**: None — the backend API is already complete. No database migrations.
+**Existing test at line 152–155 — update assertion:**
-**Validation Gates**:
-- `npx vitest run` — all existing and new unit tests pass
-- Playwright E2E on Firefox — all 7 new scenarios pass
-- `npx tsc --noEmit` — 0 errors
-- `make lint-fast` — 0 new warnings
+The test currently documents the wrong behavior:
+
+```ts
+it('returns false for expiring LE cert not in use', () => {
+ const cert: Certificate = { id: 7, name: 'Exp', domain: 'd', issuer: 'LE', expires_at: '', status: 'expiring', provider: 'letsencrypt' }
+ expect(isDeletable(cert, noHosts)).toBe(false) // wrong — must become true
+})
+```
+
+**Changes:**
+- Rename description to `'returns true for expiring LE cert not in use'`
+- Change assertion to `toBe(true)`
+
+**Add** a new test case to guard the in-use path:
+```ts
+it('returns false for expiring LE cert in use', () => {
+ const cert: Certificate = { id: 8, name: 'ExpUsed', domain: 'd', issuer: 'LE', expires_at: '', status: 'expiring', provider: 'letsencrypt' }
+ expect(isDeletable(cert, withHost(8))).toBe(false)
+})
+```
+
+### 3b. `frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx`
+
+The current `certs` fixture (lines 28–30) covers: `custom/valid`, `letsencrypt-staging/untrusted`, `letsencrypt/expired`. No `expiring` fixture exists.
+
+**Add** a standalone test verifying `providerLabel` renders the correct label for an expiring cert:
+```ts
+it('renders "Expiring LE" label for expiring letsencrypt certificate', () => {
+ render(
+
+ )
+ expect(screen.getByText('Expiring LE')).toBeInTheDocument()
+})
+```
+
+---
+
+## 4. Backend Verification
+
+**No backend change required.**
+
+`DELETE /api/v1/certificates/:id` is handled by `CertificateHandler.Delete` in `backend/internal/api/handlers/certificate_handler.go` (line 140). The only guard is `IsCertificateInUse` (line 156). There is no status-based check — the handler invokes `service.DeleteCertificate(uint(id))` unconditionally once the in-use check passes.
+
+`CertificateService.DeleteCertificate` in `backend/internal/services/certificate_service.go` (line 396) likewise only inspects `IsCertificateInUse` before proceeding. The cert's `Status` field is never read or compared during deletion.
+
+A cert with `status = 'expiring'` that is not in use is deleted successfully by the backend today; the bug is frontend-only.
+
+---
+
+## 5. E2E Consideration
+
+**File:** `tests/certificate-bulk-delete.spec.ts`
+
+The spec currently has no fixture that places a cert into `expiring` status, because status is computed server-side at query time from the actual expiry date. Manufacturing an `expiring` cert in Playwright requires inserting a certificate whose expiry falls within the renewal window (≈30 days out).
+
+**Recommended addition:** Add a test scenario that uploads a custom certificate with an expiry date 15 days from now and is not attached to any proxy host, then asserts:
+1. Its row checkbox is enabled and its per-row delete button is present.
+2. It can be selected and bulk-deleted via `BulkDeleteCertificateDialog`.
+
+If producing a near-expiry cert at the E2E layer is not feasible, coverage from §3a and §3b is sufficient for this fix and the E2E test may be deferred to a follow-up.
+
+---
+
+## 6. Commit Slicing Strategy
+
+**Single PR.** This is a self-contained bug fix with no API contract changes and no migration.
+
+Suggested commit message:
+```
+fix(certificates): allow deletion of expiring certs not in use
+
+Expiring Let's Encrypt certs not attached to any proxy host were
+undeletable because isDeletable only permitted deletion when
+status === 'expired'. Extends the condition to also allow
+status === 'expiring', updates providerLabel and getWarningKey for
+consistent UX, and corrects the existing unit test that was asserting
+the wrong behavior.
+```
+
+**Files changed:**
+- `frontend/src/components/CertificateList.tsx`
+- `frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx`
+- `frontend/src/components/dialogs/DeleteCertificateDialog.tsx`
+- `frontend/src/locales/en/translation.json`
+- `frontend/src/components/__tests__/CertificateList.test.tsx`
+- `frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx`
diff --git a/frontend/src/components/CertificateList.tsx b/frontend/src/components/CertificateList.tsx
index e4802324..cd35e821 100644
--- a/frontend/src/components/CertificateList.tsx
+++ b/frontend/src/components/CertificateList.tsx
@@ -20,6 +20,7 @@ type SortColumn = 'name' | 'expires'
type SortDirection = 'asc' | 'desc'
export function isInUse(cert: Certificate, hosts: ProxyHost[]): boolean {
+ if (!cert.id) return false
return hosts.some(h => (h.certificate_id ?? h.certificate?.id) === cert.id)
}
@@ -29,7 +30,8 @@ export function isDeletable(cert: Certificate, hosts: ProxyHost[]): boolean {
return (
cert.provider === 'custom' ||
cert.provider === 'letsencrypt-staging' ||
- cert.status === 'expired'
+ cert.status === 'expired' ||
+ cert.status === 'expiring'
)
}
@@ -234,7 +236,7 @@ export default function CertificateList() {
sortedCertificates.map((cert) => {
const inUse = isInUse(cert, hosts)
const deletable = isDeletable(cert, hosts)
- const isInUseDeletableCategory = inUse && (cert.provider === 'custom' || cert.provider === 'letsencrypt-staging' || cert.status === 'expired')
+ const isInUseDeletableCategory = inUse && (cert.provider === 'custom' || cert.provider === 'letsencrypt-staging' || cert.status === 'expired' || cert.status === 'expiring')
return (
diff --git a/frontend/src/components/__tests__/CertificateList.test.tsx b/frontend/src/components/__tests__/CertificateList.test.tsx
index 43097b12..ea63b910 100644
--- a/frontend/src/components/__tests__/CertificateList.test.tsx
+++ b/frontend/src/components/__tests__/CertificateList.test.tsx
@@ -150,9 +150,14 @@ describe('CertificateList', () => {
expect(isDeletable(cert, noHosts)).toBe(false)
})
- it('returns false for expiring LE cert not in use', () => {
+ it('returns true for expiring LE cert not in use', () => {
const cert: Certificate = { id: 7, name: 'Exp', domain: 'd', issuer: 'LE', expires_at: '', status: 'expiring', provider: 'letsencrypt' }
- expect(isDeletable(cert, noHosts)).toBe(false)
+ expect(isDeletable(cert, noHosts)).toBe(true)
+ })
+
+ it('returns false for expiring LE cert that is in use', () => {
+ const cert: Certificate = { id: 7, name: 'Exp', domain: 'd', issuer: 'LE', expires_at: '', status: 'expiring', provider: 'letsencrypt' }
+ expect(isDeletable(cert, withHost(7))).toBe(false)
})
})
@@ -172,6 +177,12 @@ describe('CertificateList', () => {
const cert: Certificate = { id: 99, domain: 'd', issuer: 'X', expires_at: '', status: 'valid', provider: 'custom' }
expect(isInUse(cert, [createProxyHost({ certificate_id: 3 })])).toBe(false)
})
+
+ it('returns false when cert.id is undefined even if a host has certificate_id undefined', () => {
+ const cert: Certificate = { domain: 'd', issuer: 'X', expires_at: '', status: 'valid', provider: 'custom' }
+ const host = createProxyHost({ certificate_id: undefined })
+ expect(isInUse(cert, [host])).toBe(false)
+ })
})
it('renders delete button for deletable certs', async () => {
diff --git a/frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx b/frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx
index 95ef012b..4db633ab 100644
--- a/frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx
+++ b/frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx
@@ -25,6 +25,7 @@ function providerLabel(cert: Certificate): string {
if (cert.provider === 'letsencrypt-staging') return 'Staging'
if (cert.provider === 'custom') return 'Custom'
if (cert.status === 'expired') return 'Expired LE'
+ if (cert.status === 'expiring') return 'Expiring LE'
return cert.provider
}
diff --git a/frontend/src/components/dialogs/DeleteCertificateDialog.tsx b/frontend/src/components/dialogs/DeleteCertificateDialog.tsx
index 03fbf23f..68491eb6 100644
--- a/frontend/src/components/dialogs/DeleteCertificateDialog.tsx
+++ b/frontend/src/components/dialogs/DeleteCertificateDialog.tsx
@@ -23,6 +23,7 @@ interface DeleteCertificateDialogProps {
function getWarningKey(cert: Certificate): string {
if (cert.status === 'expired') return 'certificates.deleteConfirmExpired'
+ if (cert.status === 'expiring') return 'certificates.deleteConfirmExpiring'
if (cert.provider === 'letsencrypt-staging') return 'certificates.deleteConfirmStaging'
return 'certificates.deleteConfirmCustom'
}
diff --git a/frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx b/frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx
index dd0a1991..65d4eac4 100644
--- a/frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx
+++ b/frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx
@@ -126,4 +126,18 @@ describe('BulkDeleteCertificateDialog', () => {
)
expect(container.innerHTML).toBe('')
})
+
+ it('renders "Expiring LE" label for a letsencrypt cert with status expiring', () => {
+ const expiringCert = makeCert({ id: 4, name: 'Expiring Cert', domain: 'expiring.example.com', provider: 'letsencrypt', status: 'expiring' })
+ render(
+
+ )
+ expect(screen.getByText('Expiring LE')).toBeInTheDocument()
+ })
})
diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json
index 59c6e0b3..fde5acb9 100644
--- a/frontend/src/locales/de/translation.json
+++ b/frontend/src/locales/de/translation.json
@@ -179,6 +179,7 @@
"deleteConfirmCustom": "This will permanently delete this certificate. A backup will be created first.",
"deleteConfirmStaging": "This staging certificate will be removed. It will be regenerated on next request.",
"deleteConfirmExpired": "This expired certificate is no longer active and will be permanently removed.",
+ "deleteConfirmExpiring": "This certificate is expiring soon. It will be permanently removed and will not be auto-renewed.",
"deleteSuccess": "Certificate deleted",
"deleteFailed": "Failed to delete certificate",
"deleteInUse": "Cannot delete — certificate is attached to a proxy host",
diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json
index 181a97e3..5dbcda48 100644
--- a/frontend/src/locales/en/translation.json
+++ b/frontend/src/locales/en/translation.json
@@ -182,12 +182,13 @@
"uploadSuccess": "Certificate uploaded successfully",
"uploadFailed": "Failed to upload certificate",
"note": "Note",
- "noteText": "You can delete custom certificates, staging certificates, and expired production certificates that are not attached to any proxy host. Active production certificates are automatically renewed by Caddy.",
+ "noteText": "You can delete custom certificates, staging certificates, and expired or expiring production certificates that are not attached to any proxy host. Active production certificates are automatically renewed by Caddy.",
"provider": "Provider",
"deleteTitle": "Delete Certificate",
"deleteConfirmCustom": "This will permanently delete this certificate. A backup will be created first.",
"deleteConfirmStaging": "This staging certificate will be removed. It will be regenerated on next request.",
"deleteConfirmExpired": "This expired certificate is no longer active and will be permanently removed.",
+ "deleteConfirmExpiring": "This certificate is expiring soon. It will be permanently removed and will not be auto-renewed.",
"deleteSuccess": "Certificate deleted",
"deleteFailed": "Failed to delete certificate",
"deleteInUse": "Cannot delete — certificate is attached to a proxy host",
diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json
index 6452f8e0..6e525ffc 100644
--- a/frontend/src/locales/es/translation.json
+++ b/frontend/src/locales/es/translation.json
@@ -179,6 +179,7 @@
"deleteConfirmCustom": "This will permanently delete this certificate. A backup will be created first.",
"deleteConfirmStaging": "This staging certificate will be removed. It will be regenerated on next request.",
"deleteConfirmExpired": "This expired certificate is no longer active and will be permanently removed.",
+ "deleteConfirmExpiring": "This certificate is expiring soon. It will be permanently removed and will not be auto-renewed.",
"deleteSuccess": "Certificate deleted",
"deleteFailed": "Failed to delete certificate",
"deleteInUse": "Cannot delete — certificate is attached to a proxy host",
diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json
index b41448a2..3c927858 100644
--- a/frontend/src/locales/fr/translation.json
+++ b/frontend/src/locales/fr/translation.json
@@ -179,6 +179,7 @@
"deleteConfirmCustom": "This will permanently delete this certificate. A backup will be created first.",
"deleteConfirmStaging": "This staging certificate will be removed. It will be regenerated on next request.",
"deleteConfirmExpired": "This expired certificate is no longer active and will be permanently removed.",
+ "deleteConfirmExpiring": "This certificate is expiring soon. It will be permanently removed and will not be auto-renewed.",
"deleteSuccess": "Certificate deleted",
"deleteFailed": "Failed to delete certificate",
"deleteInUse": "Cannot delete — certificate is attached to a proxy host",
diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json
index 7e17a015..265f6f85 100644
--- a/frontend/src/locales/zh/translation.json
+++ b/frontend/src/locales/zh/translation.json
@@ -179,6 +179,7 @@
"deleteConfirmCustom": "This will permanently delete this certificate. A backup will be created first.",
"deleteConfirmStaging": "This staging certificate will be removed. It will be regenerated on next request.",
"deleteConfirmExpired": "This expired certificate is no longer active and will be permanently removed.",
+ "deleteConfirmExpiring": "This certificate is expiring soon. It will be permanently removed and will not be auto-renewed.",
"deleteSuccess": "Certificate deleted",
"deleteFailed": "Failed to delete certificate",
"deleteInUse": "Cannot delete — certificate is attached to a proxy host",
|