Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions frontend/src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import { Fault, Mark } from "@/features/desk/primitives";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { DashboardPage, PageHeader } from "@/components/dashboard-ui";

export default function Error({ reset }: { reset: () => void }) {
return <main className="standalone-state"><Link className="brand" href="/"><Mark />ackvia.</Link><Fault error={{ title: "The investigation desk could not load", message: "Your captured evidence is stored separately. Try loading the workspace again." }} onRetry={reset} /><Link className="button" href="/">Open the desk</Link></main>;
return <AppShell><DashboardPage><PageHeader title="Something went wrong" description="Ackvia could not render this workspace view. Your captured evidence is stored separately." actions={<><button className="dash-button" type="button" onClick={reset}>Try again</button><Link className="dash-button" href="/overview">Open Overview</Link></>} /></DashboardPage></AppShell>;
}
5 changes: 3 additions & 2 deletions frontend/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Mark } from "@/features/desk/primitives";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { DashboardPage, PageHeader } from "@/components/dashboard-ui";

export default function NotFound() {
return <main className="standalone-state"><Link className="brand" href="/"><Mark />ackvia.</Link><h1>This route doesn’t exist.</h1><p className="muted">Return to the investigation desk to choose a form and submission.</p><Link className="button" href="/">Open the desk</Link></main>;
return <AppShell><DashboardPage><PageHeader title="Page not found" description="This route does not exist in the Ackvia workspace." actions={<Link className="dash-button" href="/overview">Return to Overview</Link>} /></DashboardPage></AppShell>;
}
11 changes: 6 additions & 5 deletions frontend/src/features/dashboard/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link";
import { ArrowLeft, CheckCircle2, RefreshCw, Webhook } from "lucide-react";
import { AppShell } from "@/components/app-shell";
import { CodeBlock, DashboardPage, DataTable, DetailFields, EmptyState, HealthBadge, Panel, PanelTitle, PageHeader, StatStrip, StatusBadge, Timeline } from "@/components/dashboard-ui";
import { attempts, clients, deliveries, getClientForSite, getDeliveryForAttempt, getMockAttempt, getMockClient, getMockDelivery, getMockIncident, getMockLead, getMockLeadPath, getMockSite, incidents, leadPaths, leads, sites } from "@/lib/mock-data";
import { attempts, clients, deliveries, getClientForSite, getMockAttempt, getMockClient, getMockDelivery, getMockIncident, getMockLead, getMockLeadPath, getMockSite, incidents, leadPaths, leads, sites } from "@/lib/mock-data";

const deliveryTone = { Pending: "neutral", Processing: "warn", "Awaiting retry": "warn", Delivered: "good", Failed: "bad", Unknown: "neutral" } as const;
const attemptTone = { Succeeded: "good", "Retryable failure": "warn", "Permanent failure": "bad", "Unknown result": "neutral" } as const;
Expand All @@ -21,7 +21,7 @@ export function ClientDetailPage({ clientId }: { clientId: string }) {
if (!client) return <ShellPage><EmptyState title="Client not found" description="This mock client ID does not exist." /></ShellPage>;
const clientSites = sites.filter((site) => site.clientId === client.id);
const clientLeads = leads.filter((lead) => lead.clientId === client.id);
return <ShellPage><PageHeader title={client.name} description="Client-level lead reliability and delivery health." breadcrumbs={[{ label: "Clients", href: "/clients" }, { label: client.name }]} actions={<HealthBadge status={client.health} />} /><StatStrip items={[{ label: "Sites", value: String(client.siteIds.length) }, { label: "Lead paths", value: String(client.leadPathCount) }, { label: "Captured leads", value: String(client.leads) }, { label: "Delivery rate", value: client.deliveryRate }, { label: "Active incidents", value: String(client.incidents) }]} /><div className="split-panels"><Panel><PanelTitle title="Sites" /><div className="inline-list">{clientSites.map((site) => <Link href={`/sites/${site.id}`} key={site.id}><span><strong>{site.name}</strong><small>{site.domain}</small></span><HealthBadge status={site.health} /></Link>)}</div></Panel><Panel><PanelTitle title="Active incidents" />{client.incidents ? <div className="inline-list"><Link href="/incidents/incident-keller-crm"><span><strong>CRM Webhook delivery failures</strong><small>Started 08:43:03 · 31 affected leads</small></span><StatusBadge label="Resolved automatically" tone="good" /></Link></div> : <EmptyState title="No active incidents" />}</Panel></div><Panel><PanelTitle title="Recent leads" /><DataTable headers={["Lead", "Captured", "Site / path", "Capture state", "Deliveries"]}>{clientLeads.map((lead) => <tr key={lead.id}><td><Link href={`/leads/${lead.id}`}><strong>{lead.id}</strong></Link><small>{lead.customerName}</small></td><td className="mono">{lead.capturedAt}</td><td>{getMockSite(lead.siteId)?.domain}<small>{getMockLeadPath(lead.leadPathId)?.name}</small></td><td><StatusBadge label={lead.state} tone="good" /></td><td>{lead.deliveryIds.length} destinations</td></tr>)}</DataTable></Panel></ShellPage>;
return <ShellPage><PageHeader title={client.name} description="Client-level lead reliability and delivery health." breadcrumbs={[{ label: "Clients", href: "/clients" }, { label: client.name }]} actions={<HealthBadge status={client.health} />} /><StatStrip items={[{ label: "Sites", value: String(client.siteIds.length) }, { label: "Lead paths", value: String(client.leadPathCount) }, { label: "Captured leads", value: String(client.leads) }, { label: "Delivery rate", value: client.deliveryRate }, { label: "Incidents", value: String(client.incidents) }]} /><div className="split-panels"><Panel><PanelTitle title="Sites" /><div className="inline-list">{clientSites.map((site) => <Link href={`/sites/${site.id}`} key={site.id}><span><strong>{site.name}</strong><small>{site.domain}</small></span><HealthBadge status={site.health} /></Link>)}</div></Panel><Panel><PanelTitle title="Recent incidents" />{client.incidents ? <div className="inline-list"><Link href="/incidents/incident-keller-crm"><span><strong>CRM Webhook delivery failures</strong><small>Started 08:43:03 · 31 affected leads</small></span><StatusBadge label="Resolved automatically" tone="good" /></Link></div> : <EmptyState title="No active incidents" />}</Panel></div><Panel><PanelTitle title="Recent leads" /><DataTable headers={["Lead", "Captured", "Site / path", "Capture state", "Deliveries"]}>{clientLeads.map((lead) => <tr key={lead.id}><td><Link href={`/leads/${lead.id}`}><strong>{lead.id}</strong></Link><small>{lead.customerName}</small></td><td className="mono">{lead.capturedAt}</td><td>{getMockSite(lead.siteId)?.domain}<small>{getMockLeadPath(lead.leadPathId)?.name}</small></td><td><StatusBadge label={lead.state} tone="good" /></td><td>{lead.deliveryIds.length} destinations</td></tr>)}</DataTable></Panel></ShellPage>;
}

export function SitesPage() {
Expand Down Expand Up @@ -53,7 +53,7 @@ export function LeadDetailPage({ leadId }: { leadId: string }) {
if (!lead) return <ShellPage><EmptyState title="Lead not found" description="This mock lead ID does not exist." /></ShellPage>;
const site = getMockSite(lead.siteId);
const deliveriesForLead = deliveries.filter((delivery) => lead.deliveryIds.includes(delivery.id));
return <ShellPage><PageHeader title={lead.id} description={`${site?.name} / ${getMockLeadPath(lead.leadPathId)?.name}`} breadcrumbs={[{ label: "Leads", href: "/leads" }, { label: lead.id }]} actions={<StatusBadge label="Captured safely" tone="good" />} /><div className="detail-grid"><div className="detail-stack"><Panel><PanelTitle title="Lead information" /><DetailFields fields={[{ label: "Customer", value: lead.customerName }, { label: "Email", value: lead.email }, { label: "Phone", value: lead.phone, mono: true }, { label: "Captured", value: lead.capturedAt, mono: true }, { label: "Site", value: site ? <Link className="entity-link" href={`/sites/${site.id}`}>{site.domain}</Link> : "Unknown" }, { label: "Lead path", value: getMockLeadPath(lead.leadPathId)?.name ?? "Unknown" }, { label: "Capture state", value: <StatusBadge label="Captured safely" tone="good" /> }, { label: "Message", value: lead.message }]} /><CodeBlock title="Raw payload">{JSON.stringify({ name: lead.customerName, email: lead.email, phone: lead.phone, message: lead.message }, null, 2)}</CodeBlock></Panel><Panel><PanelTitle title="Lead lifecycle" /><Timeline items={[{ label: "Captured", time: "08:42:31", detail: "Received from kellersolar.com" }, { label: "Durably stored", time: "08:42:31", detail: "Saved to database" }]} /></Panel></div><div className="detail-stack"><Panel><PanelTitle title="Independent deliveries" />{deliveriesForLead.map((delivery) => <Link className="attempt-row" href={`/deliveries/${delivery.id}`} key={delivery.id}><span className="site-icon"><Webhook size={14} /></span><span><strong>{delivery.destination}</strong><small>{delivery.attemptIds.length} attempt{delivery.attemptIds.length === 1 ? "" : "s"}</small></span><DeliveryBadge state={delivery.state} /></Link>)}</Panel><Panel><PanelTitle title="Delivery evidence" /><Timeline items={deliveriesForLead.map((delivery) => ({ label: `${delivery.destination} · ${delivery.state}`, time: delivery.deliveredAt ?? delivery.createdAt, tone: delivery.state === "Delivered" ? "good" : "warn" }))} /></Panel></div></div></ShellPage>;
return <ShellPage><PageHeader title={lead.id} description={`${site?.name} · ${site?.domain} · ${getMockLeadPath(lead.leadPathId)?.name} · ${lead.capturedAt}`} breadcrumbs={[{ label: "Leads", href: "/leads" }, { label: lead.id }]} actions={<StatusBadge label="Captured safely" tone="good" />} /><div className="detail-grid"><div className="detail-stack"><Panel><PanelTitle title="Lead information" /><DetailFields fields={[{ label: "Customer", value: lead.customerName }, { label: "Email", value: lead.email }, { label: "Phone", value: lead.phone, mono: true }, { label: "Captured", value: lead.capturedAt, mono: true }, { label: "Site", value: site ? <Link className="entity-link" href={`/sites/${site.id}`}>{site.domain}</Link> : "Unknown" }, { label: "Lead path", value: getMockLeadPath(lead.leadPathId)?.name ?? "Unknown" }, { label: "Capture state", value: <StatusBadge label="Captured safely" tone="good" /> }, { label: "Message", value: lead.message }]} /><CodeBlock title="Raw payload">{JSON.stringify({ name: lead.customerName, email: lead.email, phone: lead.phone, message: lead.message }, null, 2)}</CodeBlock></Panel><Panel><PanelTitle title="Lead lifecycle" /><Timeline items={[{ label: "Captured", time: "08:42:31", detail: "Received from kellersolar.com" }, { label: "Durably stored", time: "08:42:31", detail: "Saved to database" }]} /></Panel></div><div className="detail-stack"><Panel><PanelTitle title="Deliveries" />{deliveriesForLead.map((delivery) => <Link className="attempt-row" href={`/deliveries/${delivery.id}`} key={delivery.id}><span className="site-icon"><Webhook size={14} /></span><span><strong>{delivery.destination}</strong><small>{delivery.attemptIds.length} attempt{delivery.attemptIds.length === 1 ? "" : "s"}</small></span><DeliveryBadge state={delivery.state} /></Link>)}</Panel></div></div></ShellPage>;
}

export function IncidentsPage() {
Expand Down Expand Up @@ -81,12 +81,13 @@ export function DeliveryDetailPage({ deliveryId }: { deliveryId: string }) {

export function AttemptDetailPage({ deliveryId, attemptId }: { deliveryId: string; attemptId: string }) {
const attempt = getMockAttempt(attemptId);
const delivery = attempt ? getDeliveryForAttempt(attempt.id) : getMockDelivery(deliveryId);
const delivery = attempt?.deliveryId === deliveryId ? getMockDelivery(deliveryId) : undefined;
if (!attempt || !delivery) return <ShellPage><EmptyState title="Attempt not found" description="This mock attempt ID does not exist." /></ShellPage>;
const lead = getMockLead(delivery.leadId);
const previous = getMockAttempt(delivery.attemptIds[attempt.number - 2] ?? "");
const next = getMockAttempt(delivery.attemptIds[attempt.number] ?? "");
return <ShellPage><PageHeader title={`${delivery.destination} — Attempt #${attempt.number}`} description={`Technical evidence for ${lead?.id ?? delivery.leadId}`} breadcrumbs={[{ label: "Leads", href: "/leads" }, { label: lead?.id ?? delivery.leadId, href: `/leads/${delivery.leadId}` }, { label: delivery.destination, href: `/deliveries/${delivery.id}` }, { label: `Attempt #${attempt.number}` }]} actions={<AttemptBadge result={attempt.result} />} /><div className="attempt-navigation">{previous ? <Link href={`/deliveries/${delivery.id}/attempts/${previous.id}`}><ArrowLeft size={13} />Attempt #{previous.number}</Link> : <span />} {next && <Link href={`/deliveries/${delivery.id}/attempts/${next.id}`}>Attempt #{next.number}<ChevronRightIcon /></Link>}</div><div className="detail-grid"><div className="detail-stack"><Panel><PanelTitle title="Summary" /><DetailFields fields={[{ label: "Attempt #", value: attempt.number, mono: true }, { label: "Trigger", value: attempt.trigger }, { label: "Started", value: attempt.startedAt, mono: true }, { label: "Finished", value: attempt.finishedAt, mono: true }, { label: "Duration", value: attempt.duration, mono: true }, { label: "Ackvia result", value: <AttemptBadge result={attempt.result} /> }, { label: "Next retry", value: attempt.nextRetry ?? "—", mono: true }]} /></Panel><Panel><PanelTitle title="Error" /><DetailFields fields={[{ label: "Provider response", value: `${attempt.providerStatus} — ${attempt.providerMessage}`, mono: true }, { label: "Category", value: attempt.result === "Succeeded" ? "Success" : "Retryable" }]} /></Panel></div><div className="detail-stack"><Panel><PanelTitle title="Destination" /><DetailFields fields={[{ label: "Delivery", value: <Link className="entity-link" href={`/deliveries/${delivery.id}`}>{delivery.destination}</Link> }, { label: "Lead", value: <Link className="entity-link" href={`/leads/${delivery.leadId}`}>{delivery.leadId}</Link> }, { label: "Endpoint", value: "https://crm.kellersolar.com/api/leads", mono: true }, { label: "Method", value: "POST", mono: true }, { label: "Timeout", value: "10 s", mono: true }]} /></Panel><Panel><PanelTitle title="Timing" /><div className="timing-bars"><span>DNS <i style={{ width: "18%" }} /></span><span>Connect <i style={{ width: "32%" }} /></span><span>TLS <i style={{ width: "48%" }} /></span><span>Request <i style={{ width: "74%" }} /></span><span>Response <i style={{ width: "28%" }} /></span></div></Panel></div></div><div className="split-panels"><Panel><PanelTitle title="Request" /><CodeBlock title="JSON">{JSON.stringify({ name: lead?.customerName, email: lead?.email, message: lead?.message }, null, 2)}</CodeBlock></Panel><Panel><PanelTitle title="Response" /><CodeBlock title={attempt.providerStatus}>{JSON.stringify({ status: attempt.providerStatus, message: attempt.providerMessage }, null, 2)}</CodeBlock></Panel></div></ShellPage>;
const classification = attempt.result === "Succeeded" ? "Success" : attempt.result === "Retryable failure" ? "Retryable" : attempt.result === "Permanent failure" ? "Permanent" : "Unknown";
return <ShellPage><PageHeader title={`${delivery.destination} — Attempt #${attempt.number}`} description={`Technical evidence for ${lead?.id ?? delivery.leadId}`} breadcrumbs={[{ label: "Leads", href: "/leads" }, { label: lead?.id ?? delivery.leadId, href: `/leads/${delivery.leadId}` }, { label: delivery.destination, href: `/deliveries/${delivery.id}` }, { label: `Attempt #${attempt.number}` }]} actions={<AttemptBadge result={attempt.result} />} /><div className="attempt-navigation">{previous ? <Link href={`/deliveries/${delivery.id}/attempts/${previous.id}`}><ArrowLeft size={13} />Attempt #{previous.number}</Link> : <span />} {next && <Link href={`/deliveries/${delivery.id}/attempts/${next.id}`}>Attempt #{next.number}<ChevronRightIcon /></Link>}</div><div className="detail-grid"><div className="detail-stack"><Panel><PanelTitle title="Summary" /><DetailFields fields={[{ label: "Attempt #", value: attempt.number, mono: true }, { label: "Trigger", value: attempt.trigger }, { label: "Started", value: attempt.startedAt, mono: true }, { label: "Finished", value: attempt.finishedAt, mono: true }, { label: "Duration", value: attempt.duration, mono: true }, { label: "Ackvia result", value: <AttemptBadge result={attempt.result} /> }, { label: "Next retry", value: attempt.nextRetry ?? "—", mono: true }]} /></Panel><Panel><PanelTitle title="Provider evidence" /><DetailFields fields={[{ label: "Provider response", value: `${attempt.providerStatus} — ${attempt.providerMessage}`, mono: true }, { label: "Ackvia classification", value: classification }]} /></Panel></div><div className="detail-stack"><Panel><PanelTitle title="Destination" /><DetailFields fields={[{ label: "Delivery", value: <Link className="entity-link" href={`/deliveries/${delivery.id}`}>{delivery.destination}</Link> }, { label: "Lead", value: <Link className="entity-link" href={`/leads/${delivery.leadId}`}>{delivery.leadId}</Link> }, { label: "Endpoint", value: "https://crm.kellersolar.com/api/leads", mono: true }, { label: "Method", value: "POST", mono: true }, { label: "Timeout", value: "10 s", mono: true }]} /></Panel><Panel><PanelTitle title="Timing" /><div className="timing-bars"><span>DNS <i style={{ width: "18%" }} /></span><span>Connect <i style={{ width: "32%" }} /></span><span>TLS <i style={{ width: "48%" }} /></span><span>Request <i style={{ width: "74%" }} /></span><span>Response <i style={{ width: "28%" }} /></span></div></Panel></div></div><div className="split-panels"><Panel><PanelTitle title="Request" /><CodeBlock title="JSON">{JSON.stringify({ name: lead?.customerName, email: lead?.email, message: lead?.message }, null, 2)}</CodeBlock></Panel><Panel><PanelTitle title="Response" /><CodeBlock title={attempt.providerStatus}>{JSON.stringify({ status: attempt.providerStatus, message: attempt.providerMessage }, null, 2)}</CodeBlock></Panel></div></ShellPage>;
}

function ChevronRightIcon() { return <span aria-hidden="true">→</span>; }
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/overview/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export type SiteHealth = { id: string; name: string; domain: string; leads: stri

export { deliveryBreakdown };
export const activityEvents = activities;
export const siteHealth: SiteHealth[] = sites.map((site) => ({ id: site.id, name: site.name, domain: site.domain, leads: site.leads.toLocaleString(), deliveryRate: site.deliveryRate, status: site.health }));
export const siteHealth: SiteHealth[] = sites.slice(0, 5).map((site) => ({ id: site.id, name: site.name, domain: site.domain, leads: site.leads.toLocaleString(), deliveryRate: site.deliveryRate, status: site.health }));
Loading
Loading