From e19f7c617839f3fc068ee09f2aaae84a9d29ff76 Mon Sep 17 00:00:00 2001 From: WhiteMuush Date: Mon, 15 Jun 2026 22:38:36 +0200 Subject: [PATCH 1/4] feat(onboarding): show setup checklist until workspace is ready When the company has no employees or no breach API key, the dashboard shows a guided checklist (add employees, add a key, run a scan) instead of an empty dashboard, plus a short project note. --- src/app/(dashboard)/dashboard/page.tsx | 17 +++ src/components/dashboard/SetupChecklist.tsx | 132 ++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/components/dashboard/SetupChecklist.tsx diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index c306de5..7f1e906 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -21,10 +21,27 @@ import { BreachTimeline } from "@/components/dashboard/BreachTimeline" import { TopBreaches } from "@/components/dashboard/TopBreaches" import { DataTypeRadar } from "@/components/dashboard/DataTypeRadar" import { AlertVelocity } from "@/components/dashboard/AlertVelocity" +import { SetupChecklist } from "@/components/dashboard/SetupChecklist" import type { DashboardPreset } from "@/types/dashboard" export default async function DashboardPage() { const session = await auth() + + const [employeeCount, apiKeyCount] = await Promise.all([ + prisma.employee.count({ where: { companyId: session!.user.companyId } }), + prisma.apiCredential.count({ where: { companyId: session!.user.companyId } }), + ]) + + if (employeeCount === 0 || apiKeyCount === 0) { + return ( + 0} + hasApiKey={apiKeyCount > 0} + isAdmin={session!.user.role === "ADMIN"} + /> + ) + } + const [data, presets, user] = await Promise.all([ getDashboardData(session!.user.companyId), prisma.dashboardPreset.findMany({ diff --git a/src/components/dashboard/SetupChecklist.tsx b/src/components/dashboard/SetupChecklist.tsx new file mode 100644 index 0000000..66c9fde --- /dev/null +++ b/src/components/dashboard/SetupChecklist.tsx @@ -0,0 +1,132 @@ +import Link from "next/link" +import { + CheckCircle2, + Circle, + Database, + KeyRound, + ScanSearch, + ArrowRight, + ExternalLink, +} from "lucide-react" +import { cn } from "@/lib/utils" + +type Step = { + icon: typeof Database + title: string + description: string + href: string + cta: string + done: boolean +} + +export function SetupChecklist({ + hasEmployees, + hasApiKey, + isAdmin, +}: { + hasEmployees: boolean + hasApiKey: boolean + isAdmin: boolean +}) { + const steps: Step[] = [ + { + icon: Database, + title: "Add employees to monitor", + description: "Connect a corporate directory (Azure AD, Google Workspace, LDAP...) to import the people you want to watch.", + href: "/data-sources", + cta: "Connect a data source", + done: hasEmployees, + }, + { + icon: KeyRound, + title: "Add a breach intelligence key", + description: "A provider key (HIBP, Dehashed...) lets DataShield look your employees up against known breaches.", + href: "/data-api", + cta: "Add an API key", + done: hasApiKey, + }, + { + icon: ScanSearch, + title: "Run your first scan", + description: "Once people and a key are in place, scan to detect exposures and generate alerts.", + href: "/employees", + cta: "Go to employees", + done: false, + }, + ] + + return ( +
+
+
+

Welcome to DataShield

+

+ Self-hosted monitoring of your employees exposure in known data breaches. Complete + these steps to start monitoring. +

+
+ +
    + {steps.map((step, i) => ( +
  1. +
    + {step.done ? ( + + ) : ( + + )} +
    +
    +
    + +

    + {i + 1}. {step.title} +

    +
    +

    {step.description}

    + {!step.done && ( + + {step.cta} + + + )} +
    +
  2. + ))} +
+ + {!isAdmin && ( +

+ Data sources and API keys are managed by admins. Ask an admin to complete the setup. +

+ )} + +
+

Thanks for trying DataShield.

+

+ It is an open-source project built to make breach exposure visible and actionable. + Found a bug or have an idea? Your feedback genuinely helps it grow. +

+ + Open an issue or share feedback + + +
+
+
+ ) +} From 84fe0555a357af2a9a1fe9088193e6b499d7c16f Mon Sep 17 00:00:00 2001 From: WhiteMuush Date: Mon, 15 Jun 2026 22:44:50 +0200 Subject: [PATCH 2/4] feat(onboarding): move setup to ephemeral /setup page Dashboard redirects to /setup until the workspace has employees and an API key; /setup redirects back to /dashboard once ready, so the page only exists during onboarding. --- src/app/(dashboard)/dashboard/page.tsx | 12 ++---------- src/app/(dashboard)/setup/page.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 src/app/(dashboard)/setup/page.tsx diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 7f1e906..dd3f9cc 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -21,7 +21,7 @@ import { BreachTimeline } from "@/components/dashboard/BreachTimeline" import { TopBreaches } from "@/components/dashboard/TopBreaches" import { DataTypeRadar } from "@/components/dashboard/DataTypeRadar" import { AlertVelocity } from "@/components/dashboard/AlertVelocity" -import { SetupChecklist } from "@/components/dashboard/SetupChecklist" +import { redirect } from "next/navigation" import type { DashboardPreset } from "@/types/dashboard" export default async function DashboardPage() { @@ -32,15 +32,7 @@ export default async function DashboardPage() { prisma.apiCredential.count({ where: { companyId: session!.user.companyId } }), ]) - if (employeeCount === 0 || apiKeyCount === 0) { - return ( - 0} - hasApiKey={apiKeyCount > 0} - isAdmin={session!.user.role === "ADMIN"} - /> - ) - } + if (employeeCount === 0 || apiKeyCount === 0) redirect("/setup") const [data, presets, user] = await Promise.all([ getDashboardData(session!.user.companyId), diff --git a/src/app/(dashboard)/setup/page.tsx b/src/app/(dashboard)/setup/page.tsx new file mode 100644 index 0000000..c33d665 --- /dev/null +++ b/src/app/(dashboard)/setup/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from "next/navigation" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" +import { SetupChecklist } from "@/components/dashboard/SetupChecklist" + +export default async function SetupPage() { + const session = await auth() + const companyId = session!.user.companyId + + const [employeeCount, apiKeyCount] = await Promise.all([ + prisma.employee.count({ where: { companyId } }), + prisma.apiCredential.count({ where: { companyId } }), + ]) + + if (employeeCount > 0 && apiKeyCount > 0) redirect("/dashboard") + + return ( + 0} + hasApiKey={apiKeyCount > 0} + isAdmin={session!.user.role === "ADMIN"} + /> + ) +} From bf1e23f7aff63f273620b783396840bb4d1ed3f1 Mon Sep 17 00:00:00 2001 From: WhiteMuush Date: Mon, 15 Jun 2026 22:50:21 +0200 Subject: [PATCH 3/4] fix(onboarding): only redirect to setup when workspace is empty Gating the dashboard on an API key locked out workspaces that have employees but no stored key. Redirect to /setup only when there are no employees and no API key. --- src/app/(dashboard)/dashboard/page.tsx | 2 +- src/app/(dashboard)/setup/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index dd3f9cc..4177cbc 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -32,7 +32,7 @@ export default async function DashboardPage() { prisma.apiCredential.count({ where: { companyId: session!.user.companyId } }), ]) - if (employeeCount === 0 || apiKeyCount === 0) redirect("/setup") + if (employeeCount === 0 && apiKeyCount === 0) redirect("/setup") const [data, presets, user] = await Promise.all([ getDashboardData(session!.user.companyId), diff --git a/src/app/(dashboard)/setup/page.tsx b/src/app/(dashboard)/setup/page.tsx index c33d665..b4381ef 100644 --- a/src/app/(dashboard)/setup/page.tsx +++ b/src/app/(dashboard)/setup/page.tsx @@ -12,7 +12,7 @@ export default async function SetupPage() { prisma.apiCredential.count({ where: { companyId } }), ]) - if (employeeCount > 0 && apiKeyCount > 0) redirect("/dashboard") + if (employeeCount > 0 || apiKeyCount > 0) redirect("/dashboard") return ( Date: Mon, 15 Jun 2026 22:55:35 +0200 Subject: [PATCH 4/4] feat(perf): prefetch main routes in the background After first paint, warm all main route bundles during browser idle time so subsequent navigation is instant, without blocking access. --- src/app/(dashboard)/layout.tsx | 2 ++ src/components/layout/RoutePrefetcher.tsx | 31 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/components/layout/RoutePrefetcher.tsx diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 7fe2686..bc902f7 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,6 +2,7 @@ import { auth } from "@/auth" import { redirect } from "next/navigation" import { Sidebar } from "@/components/layout/Sidebar" import { Topbar } from "@/components/layout/Topbar" +import { RoutePrefetcher } from "@/components/layout/RoutePrefetcher" import { Providers } from "@/components/providers" import { getOpenAlertCount } from "@/lib/alerts" @@ -17,6 +18,7 @@ export default async function DashboardLayout({ return ( +
{ + const run = () => ROUTES.forEach((route) => router.prefetch(route)) + if ("requestIdleCallback" in window) { + const id = requestIdleCallback(run) + return () => cancelIdleCallback(id) + } + const id = setTimeout(run, 200) + return () => clearTimeout(id) + }, [router]) + + return null +}