Bring Sloth Manager's DNS/Secrets dashboard here, alongside integrations
Sloth Manager's dashboard showed per-system stats for DNS and Secrets (domains/records by type, expiry counts by type) that this app's Dashboard never had -- it only ever showed the six live integration widgets. Port those two sections over, generalized to sit alongside the integrations this app added that Sloth Manager never had. New server/src/dns/stats.ts (getDnsStats(), used by new GET /api/dns/stats): zone counts are fetched live per enabled provider (matching what the DNS page itself shows, since the zone cache table only gets a row once a zone has been synced and would undercount) -- one unreachable provider surfaces its error without blanking the rest. Record-type breakdown comes from the local cache instead, since records are only ever shown from cache elsewhere in this app too (fetching every zone's records live on every dashboard load would be far more expensive for no real accuracy gain). Dashboard.tsx gained three sections under clear headers: DNS (domain/ provider/record stat tiles + a "records by type" breakdown), Secrets (monitored/expiring/expired tiles + a "secrets by type" breakdown, computed client-side from the already-fetched secrets list, same as Sloth Manager did), and the existing Integrations widgets grouped under their own header for visual parity with the two new sections. Breakdowns render as a stacked proportion bar with a color-coded legend rather than a pie chart -- this app has no charting library anywhere yet, and a plain CSS progress bar (an idiom Tabler itself uses) gets the same "see the mix at a glance" value without adding one for a single dashboard. Verified getDnsStats() against a temp SQLite DB with real migrations and an enabled + a disabled DNS provider: enabled/total provider counts, live zone counting, and cached-record-type aggregation (sorted by count) all came back correct, and the disabled provider was correctly excluded from the zone count while still counting toward the total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
655862c94e
commit
a5ec099c13
5 files changed
+295
-5
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { db } from "../db/client.js";
|
||||
import { dnsProviders, dnsRecordsCache } from "../db/schema.js";
|
||||
import { getDnsAdapterForProvider } from "./loadProvider.js";
|
||||
|
||||
export async function getDnsStats() {
|
||||
const providers = await db.select().from(dnsProviders);
|
||||
const enabledProviders = providers.filter((p) => p.enabled);
|
||||
|
||||
// Zone counts are live (matching what the DNS page itself shows) since
|
||||
// dnsZonesCache only gets a row once a zone has been synced at least once
|
||||
// and would otherwise undercount. One unreachable provider shouldn't blank
|
||||
// the whole dashboard, so failures are caught per-provider.
|
||||
const perProvider = await Promise.all(
|
||||
enabledProviders.map(async (p) => {
|
||||
try {
|
||||
const found = await getDnsAdapterForProvider(p.id);
|
||||
const zones = found ? await found.adapter.listZones() : [];
|
||||
return { providerId: p.id, name: p.name, providerType: p.providerType, zoneCount: zones.length, error: null as string | null };
|
||||
} catch (err) {
|
||||
return {
|
||||
providerId: p.id,
|
||||
name: p.name,
|
||||
providerType: p.providerType,
|
||||
zoneCount: 0,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Records, unlike zones, are only ever shown from the local cache elsewhere
|
||||
// in this app (never fetched live per-zone), so the breakdown here matches
|
||||
// that same "as of last sync" scope rather than hitting every zone's API.
|
||||
const recordRows = await db.select({ type: dnsRecordsCache.type }).from(dnsRecordsCache);
|
||||
const recordsByType = new Map<string, number>();
|
||||
for (const r of recordRows) recordsByType.set(r.type, (recordsByType.get(r.type) ?? 0) + 1);
|
||||
|
||||
return {
|
||||
totalProviders: providers.length,
|
||||
enabledProviders: enabledProviders.length,
|
||||
totalZones: perProvider.reduce((sum, p) => sum + p.zoneCount, 0),
|
||||
perProvider,
|
||||
totalRecords: recordRows.length,
|
||||
recordsByType: [...recordsByType.entries()]
|
||||
.map(([type, count]) => ({ type, count }))
|
||||
.sort((a, b) => b.count - a.count),
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../dns/providerSchemas.js";
|
||||
import { createDnsAdapter } from "../dns/registry.js";
|
||||
import { loadDnsProviderConfig, getDnsAdapterForProvider } from "../dns/loadProvider.js";
|
||||
import { getDnsStats } from "../dns/stats.js";
|
||||
import { asyncHandler } from "../utils/asyncHandler.js";
|
||||
import { notifyDnsRecordAdded, notifyDnsRecordUpdated, notifyDnsRecordDeleted } from "../services/notify.js";
|
||||
|
||||
@@ -35,6 +36,12 @@ dnsRouter.get("/provider-fields", (_req, res) => {
|
||||
res.json({ fields: DNS_PROVIDER_FIELDS });
|
||||
});
|
||||
|
||||
// ─── Dashboard stats ─────────────────────────────────────────────────────────
|
||||
|
||||
dnsRouter.get("/stats", asyncHandler(async (_req, res) => {
|
||||
res.json(await getDnsStats());
|
||||
}));
|
||||
|
||||
// ─── Providers ───────────────────────────────────────────────────────────────
|
||||
|
||||
dnsRouter.get("/providers", asyncHandler(async (_req, res) => {
|
||||
|
||||
Reference in new issue
Block a user