diff --git a/web/src/app/app/emails/page.tsx b/web/src/app/app/emails/page.tsx index 385d3244..bd815f7b 100644 --- a/web/src/app/app/emails/page.tsx +++ b/web/src/app/app/emails/page.tsx @@ -29,6 +29,7 @@ import { XIcon, } from "lucide-react"; import { SearchInput } from "@/components/ui/field"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; import { PopoverMenu, PopoverMenuContent, @@ -60,12 +61,12 @@ const DefaultFolder = { // can proactively toast the user (continuous health reporting). const HEALTH_RANK: Record = { healthy: 0, warning: 1, error: 2 }; -function healthTone(status?: AccountStatus): { dot: string; text: string; label: string } { +function healthTone(status?: AccountStatus): { dot: string; text: string; label: string; pulse: boolean } { const h = status?.health; - if (!h) return { dot: "bg-slate-300", text: "text-slate-500", label: "—" }; - if (h.status === "healthy") return { dot: "bg-emerald-500", text: "text-emerald-600", label: `Healthy ${h.score}` }; - if (h.status === "warning") return { dot: "bg-amber-500", text: "text-amber-600", label: `At risk ${h.score}` }; - return { dot: "bg-rose-500", text: "text-rose-600", label: `Issue ${h.score}` }; + if (!h) return { dot: "bg-slate-300", text: "text-slate-500", label: "—", pulse: false }; + if (h.status === "healthy") return { dot: "bg-emerald-500", text: "text-emerald-600", label: `Healthy ${h.score}`, pulse: false }; + if (h.status === "warning") return { dot: "bg-amber-500", text: "text-amber-600", label: `At risk ${h.score}`, pulse: true }; + return { dot: "bg-rose-500", text: "text-rose-600", label: `Issue ${h.score}`, pulse: true }; } export default function AddressesPage() { @@ -199,10 +200,10 @@ export default function AddressesPage() { - - 0} /> - - + } sub="connected" /> + } sub="sending now" accent={stats.healthy > 0} /> + } sub="ramping up" /> + } sub="paused or failing" last /> @@ -478,7 +479,19 @@ function MailboxRow({ )} - {warmupLabel} + + {active ? ( + + + + / + {ws?.target_volume ?? box.warmup_base} + + + ) : ( + warmupLabel + )} + diff --git a/web/src/components/app/contacts/ContactsTable.tsx b/web/src/components/app/contacts/ContactsTable.tsx index 325e32ab..4b16b247 100644 --- a/web/src/components/app/contacts/ContactsTable.tsx +++ b/web/src/components/app/contacts/ContactsTable.tsx @@ -723,13 +723,14 @@ function ContactsTableBody({ {lead?.current_step ? ( - {lead.current_step} + {lead.current_step} ) : ( Not started diff --git a/web/src/components/app/unibox/UniboxHeader.tsx b/web/src/components/app/unibox/UniboxHeader.tsx index a2bdb9b1..613c4d58 100644 --- a/web/src/components/app/unibox/UniboxHeader.tsx +++ b/web/src/components/app/unibox/UniboxHeader.tsx @@ -8,8 +8,9 @@ // pill always sits on the left edge so it stays reachable even when // the rest of the strip scrolls horizontally. -import { ActivityIcon, LayoutGridIcon, XIcon } from "lucide-react"; +import { LayoutGridIcon, XIcon } from "lucide-react"; import useUniboxOverview from "@/lib/api/hooks/app/unibox/useUniboxOverview"; +import AnimatedNumber from "@/components/ui/AnimatedNumber"; import { cn } from "@/lib/utils"; interface UniboxHeaderProps { @@ -72,8 +73,11 @@ export function UniboxHeader({ -
- +
+ + + + live
@@ -99,7 +103,7 @@ function Stat({ tone === "accent" ? "text-sky-600" : muted ? "text-slate-400" : "text-slate-900", )} > - {value.toLocaleString()} + {label}
diff --git a/web/src/components/layout/AppNav.tsx b/web/src/components/layout/AppNav.tsx index fab179e0..81ff3375 100644 --- a/web/src/components/layout/AppNav.tsx +++ b/web/src/components/layout/AppNav.tsx @@ -13,6 +13,7 @@ import { CheckSquareIcon, CircleDollarSignIcon, FileTextIcon, + FlameIcon, GitBranchIcon, InboxIcon, KeyIcon, @@ -28,6 +29,7 @@ import { useMemo } from "react"; import { useAppStore } from "@/stores"; import useFeatureAccess from "@/hooks/useFeatureAccess"; import useCampaigns from "@/lib/api/hooks/app/campaigns/useCampaigns"; +import useEmails from "@/lib/api/hooks/app/emails/useEmails"; import { UserNav } from "./UserNav"; import { Logo } from "@/components/svg"; import { cn } from "@/lib/utils"; @@ -41,8 +43,10 @@ interface NavItem { requires?: "inbox" | "advanced"; /** Role gate — when set, sidebar hides the row entirely for non-matching roles. */ rolesAllowed?: "manage"; - /** Live indicator key — renders an ambient, realtime activity cluster. */ - indicator?: "campaigns"; + /** Live indicator key — renders an ambient, realtime activity cluster. + * Each key has its OWN motif (campaigns = dot-grid, accounts = flame) so + * the rows stay visually distinct rather than a column of identical loaders. */ + indicator?: "campaigns" | "accounts"; } // Plan badge shown on locked sidebar rows. Plan names + colors come @@ -75,7 +79,7 @@ const sections: NavSection[] = [ { label: "Email", items: [ - { title: "Accounts", url: "/app/emails", icon: MailIcon }, + { title: "Accounts", url: "/app/emails", icon: MailIcon, indicator: "accounts" }, { title: "Campaigns", url: "/app/campaigns", icon: MegaphoneIcon, indicator: "campaigns" }, { title: "Contacts", url: "/app/contacts", icon: UsersIcon }, { title: "Analytics", url: "/app/analytics", icon: BarChart3Icon }, @@ -147,6 +151,7 @@ function NavRow({ item }: { item: NavItem }) { /> {item.title} {item.indicator === "campaigns" && !locked && } + {item.indicator === "accounts" && !locked && } {planBadge ? (