Dashboard: every number leads somewhere, real counts, and new charts

- Cards and charts link to the page they're about: pending messages to the
  queue, bans to blocked IPs, report warnings to the reports, and so on,
  shown only to viewers who may open that page.
- A one-line status under the greeting: what needs a look (failed tasks,
  messages retrying, recipients given up on) or, when nothing does, what's
  there. Each phrase is a link.
- Counts from the server's own objects stand in for live metrics it can't
  report, and a live number with no source reads as unknown, not zero.
- Who uses the space: a treemap of people sized by storage, colored by how
  near their quota they are, each tile opening the account.
- Where mail is waiting: queued recipients by destination, split into
  waiting, retrying and given up, each row opening the filtered queue.
- The weekly rhythm: messages by hour and weekday, shown once metric
  history exists.
- Dashboard tabs are titled by their label.
This commit is contained in:
2026-09-19 01:50:07 -07:00
parent 5641560a91
commit 5bf09ceed5
16 changed files with 1021 additions and 10 deletions
+72
View File
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Where each number on the dashboard leads: the page where you can act on
* what it counts. Matched on the metric's name, most specific first; a card
* or chart takes the link of its first metric that has one.
*/
import { usePermissions } from '@/hooks/usePermissions';
export interface DashLink {
/** A view name from the server's layout, e.g. x:QueuedMessage. */
viewName: string;
/** The layout section the view lives in. */
section: 'Management' | 'Settings';
/** Filters applied on arrival, as the list page's own f.* URL filters. */
filters?: Record<string, string>;
/** What the link says, in a few words: "Open the queue". */
label: string;
}
const RULES: [RegExp, DashLink][] = [
[/^user\.count$/, { viewName: 'x:Account/User', section: 'Management', label: 'See people' }],
[/^domain\.count$/, { viewName: 'x:Domain', section: 'Management', label: 'See domains' }],
[/^queue\.count$/, { viewName: 'x:QueuedMessage', section: 'Management', label: 'Open the queue' }],
[
/^queue\.message-queued$/,
{ viewName: 'x:Trace/InboundDelivery', section: 'Management', label: 'See deliveries in' },
],
[/^queue\./, { viewName: 'x:Trace/OutboundDelivery', section: 'Management', label: 'See deliveries out' }],
[/^delivery\./, { viewName: 'x:Trace/OutboundDelivery', section: 'Management', label: 'See deliveries out' }],
[
/^smtp\.connection-start$/,
{ viewName: 'x:Trace/InboundDelivery', section: 'Management', label: 'See deliveries in' },
],
[/^security\.|^auth\.failed$/, { viewName: 'x:BlockedIp', section: 'Settings', label: 'See blocked IPs' }],
[/^message-ingest\.(spam|ham)$/, { viewName: 'x:SpamSettings', section: 'Settings', label: 'Spam filter' }],
[
/^incoming-report\.dmarc/,
{ viewName: 'x:DmarcExternalReport', section: 'Management', label: 'Open DMARC reports' },
],
[/^incoming-report\.tls/, { viewName: 'x:TlsExternalReport', section: 'Management', label: 'Open TLS reports' }],
[/^server\.memory$|^store\./, { viewName: 'x:Log', section: 'Management', label: 'See the logs' }],
[/^message-ingest\.|^dns\./, { viewName: 'x:Log', section: 'Management', label: 'See the logs' }],
];
export function linkForMetrics(metrics: string[]): DashLink | null {
for (const m of metrics) {
const hit = RULES.find(([re]) => re.test(m));
if (hit) return hit[1];
}
return null;
}
/** The address of a link, with its filters as the list page reads them. */
export function hrefFor(link: DashLink): string {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(link.filters ?? {})) q.set(`f.${k}`, v);
const qs = q.toString();
return `/${link.section}/${link.viewName}${qs ? `?${qs}` : ''}`;
}
/** The page a set of metrics leads to, if the viewer may open it. */
export function useDashLink(metrics: string[]): DashLink | null {
const { canViewObject } = usePermissions();
const link = linkForMetrics(metrics);
return link && canViewObject(link.viewName) ? link : null;
}