Fix inflated certificate counts in dashboard and certificates page

The dashboard overview counted all proxy hosts without a cert as ACME
certs, ignoring wildcard deduplication. The certificates page only
deduplicated the current page of results (25 rows) but used a full
count(*) for the total, so hosts on other pages covered by wildcards
were never subtracted.

Now both pages fetch all ACME hosts, apply full deduplication (cert
wildcard coverage + ACME wildcard collapsing), then paginate/count
from the deduplicated result.

Also fixes a strict-mode violation in the E2E test where DataTable's
dual mobile/desktop rendering caused getByText to match two elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-04-20 21:01:39 +02:00
parent d710ad1247
commit 4c5ad53370
3 changed files with 49 additions and 23 deletions

View File

@@ -91,7 +91,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
]);
const mtlsRoles = await listMtlsRoles().catch(() => []);
const [acmeRows, acmeTotal, certRows, usageRows] = await Promise.all([
const [allAcmeRows, certRows, usageRows] = await Promise.all([
db
.select({
id: proxyHosts.id,
@@ -102,14 +102,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
})
.from(proxyHosts)
.where(isNull(proxyHosts.certificateId))
.orderBy(proxyHosts.name)
.limit(PER_PAGE)
.offset(offset),
db
.select({ value: count() })
.from(proxyHosts)
.where(isNull(proxyHosts.certificateId))
.then(([r]) => r?.value ?? 0),
.orderBy(proxyHosts.name),
db.select().from(certificates),
db
.select({
@@ -122,7 +115,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
.where(isNotNull(proxyHosts.certificateId)),
]);
const acmeHosts: AcmeHost[] = acmeRows.map(r => ({
const allAcmeHosts: AcmeHost[] = allAcmeRows.map(r => ({
id: r.id,
name: r.name,
domains: JSON.parse(r.domains) as string[],
@@ -159,9 +152,8 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
// Filter out ACME hosts whose domains are fully covered by an existing certificate's wildcard,
// and attribute them to that certificate's usedBy list instead.
let adjustedAcmeTotal = acmeTotal;
const filteredAcmeHosts: AcmeHost[] = [];
for (const host of acmeHosts) {
for (const host of allAcmeHosts) {
let coveredByCertId: number | null = null;
for (const [certId, certDomains] of certDomainMap) {
if (host.domains.every(d => isDomainCoveredByCert(d, certDomains))) {
@@ -174,7 +166,6 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
const hosts = usageMap.get(coveredByCertId) ?? [];
hosts.push({ id: host.id, name: host.name, domains: host.domains });
usageMap.set(coveredByCertId, hosts);
adjustedAcmeTotal--;
} else {
filteredAcmeHosts.push(host);
}
@@ -195,13 +186,15 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
const coveredByWildcard = wildcardDomainSets.some(wcDomains =>
host.domains.every(d => isDomainCoveredByCert(d, wcDomains))
);
if (coveredByWildcard) {
adjustedAcmeTotal--;
} else {
if (!coveredByWildcard) {
deduplicatedAcmeHosts.push(host);
}
}
// Paginate the deduplicated ACME hosts
const adjustedAcmeTotal = deduplicatedAcmeHosts.length;
const paginatedAcmeHosts = deduplicatedAcmeHosts.slice(offset, offset + PER_PAGE);
const importedCerts: ImportedCertView[] = [];
const managedCerts: ManagedCertView[] = [];
const issuedByCa = issuedClientCerts.reduce<Map<number, IssuedClientCertificate[]>>((map, cert) => {
@@ -236,7 +229,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
return (
<CertificatesClient
acmeHosts={deduplicatedAcmeHosts}
acmeHosts={paginatedAcmeHosts}
importedCerts={importedCerts}
managedCerts={managedCerts}
caCertificates={caCertificateViews}

View File

@@ -11,6 +11,7 @@ import { count, desc, isNull, sql } from "drizzle-orm";
import { ArrowLeftRight, ShieldCheck, KeyRound } from "lucide-react";
import { ReactNode } from "react";
import { getAnalyticsSummary } from "@/src/lib/analytics-db";
import { isDomainCoveredByCert } from "@/src/lib/cert-domain-match";
type StatCard = {
label: string;
@@ -20,19 +21,51 @@ type StatCard = {
};
async function loadStats(): Promise<StatCard[]> {
const [proxyHostCountResult, acmeCertCountResult, importedCertCountResult, accessListCountResult] =
const [proxyHostCountResult, acmeRows, certRows, importedCertCountResult, accessListCountResult] =
await Promise.all([
db.select({ value: count() }).from(proxyHosts),
// Proxy hosts with no explicit cert → Caddy auto-issues one ACME cert per host
db.select({ value: count() }).from(proxyHosts).where(isNull(proxyHosts.certificateId)),
// All proxy hosts with no explicit cert (for ACME deduplication)
db.select({ domains: proxyHosts.domains }).from(proxyHosts).where(isNull(proxyHosts.certificateId)),
// All certs (for wildcard coverage check)
db.select({ id: certificates.id, type: certificates.type, domainNames: certificates.domainNames, certificatePem: certificates.certificatePem }).from(certificates),
// Imported certs with actual PEM data (valid, user-managed)
db.select({ value: count() }).from(certificates).where(
sql`${certificates.type} = 'imported' AND ${certificates.certificatePem} IS NOT NULL`
),
db.select({ value: count() }).from(accessLists)
]);
// Build cert domain map for wildcard coverage checks
const certDomainMap = new Map<number, string[]>();
for (const cert of certRows) {
certDomainMap.set(cert.id, JSON.parse(cert.domainNames) as string[]);
}
// Deduplicate ACME hosts: remove those covered by a cert's wildcard or another ACME wildcard
const acmeHostDomains = acmeRows.map(r => JSON.parse(r.domains) as string[]);
const wildcardAcmeDomainSets = acmeHostDomains.filter(domains => domains.some((d: string) => d.startsWith('*.')));
let acmeCount = 0;
for (const domains of acmeHostDomains) {
// Check if covered by an existing certificate's wildcard
let covered = false;
for (const [, certDomains] of certDomainMap) {
if (domains.every((d: string) => isDomainCoveredByCert(d, certDomains))) {
covered = true;
break;
}
}
// Check if this non-wildcard host is covered by a wildcard ACME host
if (!covered && !domains.some((d: string) => d.startsWith('*.'))) {
covered = wildcardAcmeDomainSets.some(wcDomains =>
domains.every((d: string) => isDomainCoveredByCert(d, wcDomains))
);
}
if (!covered) acmeCount++;
}
const proxyHostsCount = proxyHostCountResult[0]?.value ?? 0;
const certificatesCount = (acmeCertCountResult[0]?.value ?? 0) + (importedCertCountResult[0]?.value ?? 0);
const certificatesCount = acmeCount + (importedCertCountResult[0]?.value ?? 0);
const accessListsCount = accessListCountResult[0]?.value ?? 0;
return [

View File

@@ -107,8 +107,8 @@ test.describe('Certificates', () => {
await page.getByRole('tab', { name: /acme/i }).click();
const acmeTab = page.locator('[role="tabpanel"]');
// The wildcard host should be visible
await expect(acmeTab.getByText(`*.${domain}`)).toBeVisible({ timeout: 5_000 });
// The wildcard host should be visible (use .first() as DataTable renders both mobile card and desktop table)
await expect(acmeTab.getByText(`*.${domain}`).first()).toBeVisible({ timeout: 5_000 });
// The subdomain host should NOT appear as a separate entry
await expect(acmeTab.getByText(`sub.${domain}`)).not.toBeVisible({ timeout: 5_000 });
} finally {