BREAKING CHANGE(certs): Introduce domain-centric certificate provisioning with per-domain exponential backoff and a staggered serial scheduler; add domain-based reprovision API and UI backoff display; change certificate overview API to be domain-first and include backoff info; bump related deps.

This commit is contained in:
2026-02-15 16:03:13 +00:00
parent 2d44528345
commit 8e9de46cd2
11 changed files with 529 additions and 182 deletions

View File

@@ -94,13 +94,13 @@ export class OpsViewCertificates extends DeesElement {
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
}
.domainPills {
.routePills {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.domainPill {
.routePill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
@@ -125,6 +125,17 @@ export class OpsViewCertificates extends DeesElement {
white-space: nowrap;
}
.backoffIndicator {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
padding: 2px 6px;
border-radius: 4px;
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
}
.expiryInfo {
font-size: 12px;
}
@@ -218,14 +229,16 @@ export class OpsViewCertificates extends DeesElement {
<dees-table
.data=${this.certState.certificates}
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
Route: cert.routeName,
Domains: this.renderDomainPills(cert.domains),
Domain: cert.domain,
Routes: this.renderRoutePills(cert.routeNames),
Status: this.renderStatusBadge(cert.status),
Source: this.renderSourceBadge(cert.source),
Expires: this.renderExpiry(cert.expiryDate),
Error: cert.error
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
: '',
Error: cert.backoffInfo
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
: cert.error
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
: '',
})}
.dataActions=${[
{
@@ -245,11 +258,11 @@ export class OpsViewCertificates extends DeesElement {
}
await appstate.certificateStatePart.dispatchAction(
appstate.reprovisionCertificateAction,
cert.routeName,
cert.domain,
);
const { DeesToast } = await import('@design.estate/dees-catalog');
DeesToast.show({
message: `Reprovisioning triggered for ${cert.routeName}`,
message: `Reprovisioning triggered for ${cert.domain}`,
type: 'success',
duration: 3000,
});
@@ -263,7 +276,7 @@ export class OpsViewCertificates extends DeesElement {
const cert = actionData.item;
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: `Certificate: ${cert.routeName}`,
heading: `Certificate: ${cert.domain}`,
content: html`
<div style="padding: 20px;">
<dees-dataview-codebox
@@ -275,10 +288,10 @@ export class OpsViewCertificates extends DeesElement {
`,
menuOptions: [
{
name: 'Copy Route Name',
name: 'Copy Domain',
iconName: 'copy',
action: async () => {
await navigator.clipboard.writeText(cert.routeName);
await navigator.clipboard.writeText(cert.domain);
},
},
],
@@ -287,7 +300,7 @@ export class OpsViewCertificates extends DeesElement {
},
]}
heading1="Certificate Status"
heading2="TLS certificates across all routes"
heading2="TLS certificates by domain"
searchable
.pagination=${true}
.paginationSize=${50}
@@ -296,14 +309,14 @@ export class OpsViewCertificates extends DeesElement {
`;
}
private renderDomainPills(domains: string[]): TemplateResult {
private renderRoutePills(routeNames: string[]): TemplateResult {
const maxShow = 3;
const visible = domains.slice(0, maxShow);
const remaining = domains.length - maxShow;
const visible = routeNames.slice(0, maxShow);
const remaining = routeNames.length - maxShow;
return html`
<span class="domainPills">
${visible.map((d) => html`<span class="domainPill">${d}</span>`)}
<span class="routePills">
${visible.map((r) => html`<span class="routePill">${r}</span>`)}
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
</span>
`;
@@ -352,4 +365,16 @@ export class OpsViewCertificates extends DeesElement {
</span>
`;
}
private formatRetryTime(retryAfter?: string): string {
if (!retryAfter) return 'soon';
const retryDate = new Date(retryAfter);
const now = new Date();
const diffMs = retryDate.getTime() - now.getTime();
if (diffMs <= 0) return 'now';
const diffMin = Math.ceil(diffMs / 60000);
if (diffMin < 60) return `in ${diffMin}m`;
const diffHours = Math.ceil(diffMin / 60);
return `in ${diffHours}h`;
}
}