feat: dashboard unified widgets for all services

Implement 8 rich dashboard widgets replacing placeholder stat cards:
- ThreatScoreWidget: SVG circular gauge (0-100) with color-coded score
- AlertFeedWidget: Real-time alert stream with mark-as-read actions
- ExposureWidget: DarkWatch exposure summary with run-scan button
- VoicePrintWidget: Enrollment/analysis counts with mini bar chart
- SpamShieldWidget: Blocked calls/SMS stats with custom rules
- HomeTitleWidget: Watched properties and recent changes
- RemoveBrokersWidget: Broker registry progress with completion bar
- QuickActionsWidget: Shortcut buttons for common tasks

Update dashboard route with responsive 2-column grid layout,
auto-refresh via 60-second intervals, Suspense boundaries,
and skeleton loading states. Update tests for new widget layout.
This commit is contained in:
2026-05-25 17:45:40 -04:00
parent 7cbcde6a6b
commit 3a8e329f02
12 changed files with 1444 additions and 106 deletions

View File

@@ -3,6 +3,39 @@ import { render } from "solid-js/web";
import { MetaProvider } from "@solidjs/meta";
import type { JSX } from "solid-js";
vi.mock("~/lib/api", () => ({
api: {
correlation: {
getStats: { query: vi.fn().mockResolvedValue({ threatScore: 25, totalAlerts: 5, bySeverity: {}, bySource: {}, activeGroups: 2, resolvedCount: 3, falsePositiveCount: 1, threatBreakdown: [] }) },
getAlerts: { query: vi.fn().mockResolvedValue({ items: [
{ id: "1", title: "New credential leak detected", severity: "HIGH", source: "DARKWATCH", createdAt: new Date().toISOString() },
{ id: "2", title: "Suspicious call", severity: "WARNING", source: "SPAMSHIELD", createdAt: new Date().toISOString() },
], total: 2, page: 1, limit: 10, totalPages: 1 }) },
resolveAlert: { mutate: vi.fn().mockResolvedValue({}) },
},
darkwatch: {
getExposures: { query: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 1, totalPages: 0 }) },
runScan: { mutate: vi.fn().mockResolvedValue({}) },
},
voiceprint: {
getEnrollments: { query: vi.fn().mockResolvedValue([]) },
getAnalyses: { query: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 10, totalPages: 0 }) },
},
spamshield: {
getStats: { query: vi.fn().mockResolvedValue({ period: "week", totalDetections: 10, spamCount: 8, notSpamCount: 2, accuracy: 80, activeRules: 3 }) },
getRules: { query: vi.fn().mockResolvedValue({ userRules: [], globalRules: [] }) },
},
hometitle: {
getProperties: { query: vi.fn().mockResolvedValue([]) },
getAlerts: { query: vi.fn().mockResolvedValue([]) },
},
removebrokers: {
getStats: { query: vi.fn().mockResolvedValue({ total: 0, byStatus: {}, totalListings: 0, listingsRemoved: 0, completionRate: 0 }) },
getBrokerRegistry: { query: vi.fn().mockResolvedValue([]) },
},
},
}));
vi.mock("@solidjs/router", () => ({
A: (props: { href?: string; children?: JSX.Element; class?: string; onClick?: () => void }) => (
<a href={props.href || "#"} class={props.class} onClick={props.onClick}>
@@ -199,14 +232,7 @@ describe("AdsPage", () => {
describe("DashboardPage", () => {
it("renders dashboard title", () => {
mount(() => <DashboardPage />);
expect(document.body.textContent).toContain("Overview");
});
it("renders stat cards with data from hooks", () => {
mount(() => <DashboardPage />);
expect(document.body.textContent).toContain("Plan Tier");
expect(document.body.textContent).toContain("Total Alerts");
expect(document.body.textContent).toContain("Active Threats");
expect(document.body.textContent).toContain("Dashboard");
});
it("renders sidebar with navigation links", () => {
@@ -220,20 +246,22 @@ describe("DashboardPage", () => {
expect(document.body.textContent).toContain("Settings");
});
it("renders activity feed", () => {
it("renders widget headers", async () => {
mount(() => <DashboardPage />);
expect(document.body.textContent).toContain("Recent Activity");
expect(document.body.textContent).toContain("New credential leak detected");
expect(document.body.textContent).toContain("VoicePrint scan completed");
await new Promise((r) => setTimeout(r, 0));
expect(document.body.textContent).toContain("Threat Score");
expect(document.body.textContent).toContain("Alert Feed");
expect(document.body.textContent).toContain("Quick Actions");
});
it("renders quick actions", () => {
it("renders QuickActionsWidget with action buttons", () => {
mount(() => <DashboardPage />);
expect(document.body.textContent).toContain("Quick Actions");
expect(document.body.textContent).toContain("Run Scan");
expect(document.body.textContent).toContain("View Alerts");
expect(document.body.textContent).toContain("Add Member");
expect(document.body.textContent).toContain("Run Report");
expect(document.body.textContent).toContain("Add to Watchlist");
expect(document.body.textContent).toContain("Upload Voice Sample");
expect(document.body.textContent).toContain("Check Number");
expect(document.body.textContent).toContain("Add Property");
expect(document.body.textContent).toContain("Start Removal");
});
});