2026-03-17 16:23:47 -04:00
'use client' ;
import { useState , useEffect } from 'react' ;
import Link from 'next/link' ;
import { Button } from '@/components/ui/button' ;
import { Tabs , TabsContent , TabsList , TabsTrigger } from '@/components/ui/tabs' ;
2026-04-01 07:18:52 -04:00
import { Dialog , DialogContent , DialogHeader , DialogTitle } from '@/components/ui/dialog' ;
2026-03-17 16:23:47 -04:00
import {
ArrowLeft , Activity , History , Calendar , Mail , RefreshCw , Loader2 ,
CheckCircle2 , XCircle , AlertTriangle , Shield , Inbox , Send ,
2026-03-31 22:38:22 -04:00
Clock , ChevronDown , ChevronRight , Users , Search , LockKeyhole , UnlockKeyhole ,
2026-04-01 10:09:38 -04:00
PauseCircle , Building2 , Check , Info , TrendingUp , ExternalLink , Eye , Trash2 ,
2026-03-17 16:23:47 -04:00
} from 'lucide-react' ;
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
import {
Table ,
TableBody ,
TableCell ,
TableHead ,
TableHeader ,
TableRow ,
} from '@/components/ui/table' ;
import { StatusBadge } from '@/components/ui/status-badge' ;
import { Checkbox } from '@/components/ui/checkbox' ;
2026-03-17 16:23:47 -04:00
import SyncScheduler from '@/components/admin/SyncScheduler' ;
function fmtDate ( d : string | null | undefined ) {
if ( ! d ) return 'Never' ;
return new Date ( d ) . toLocaleString ( undefined , {
month : 'short' , day : 'numeric' , year : 'numeric' , hour : '2-digit' , minute : '2-digit' ,
} ) ;
}
function fmtNum ( n : number | null | undefined ) {
if ( n == null ) return '—' ;
return n . toLocaleString ( ) ;
}
function StatCard ( { label , value , sub , icon : Icon , cls } : {
label : string ; value : string | number ; sub? : string ; icon? : React.ElementType ; cls? : string ;
} ) {
return (
< div className = { ` rounded-lg border p-4 flex flex-col gap-1 ${ cls ? ? '' } ` } >
< div className = "flex items-center gap-2 text-xs text-muted-foreground" >
{ Icon && < Icon className = "w-3.5 h-3.5" / > } { label }
< / div >
< div className = "text-2xl font-bold tabular-nums" > { value } < / div >
{ sub && < div className = "text-xs text-muted-foreground" > { sub } < / div > }
< / div >
) ;
}
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
function MessageStatusBadge ( { status } : { status : string } ) {
const tone =
status === 'delivered' ? 'ok' :
status === 'rejected' ? 'error' :
status === 'held' ? 'warn' :
status === 'bounced' ? 'warn' :
status === 'spam' ? 'accent' :
'inactive' ;
return < StatusBadge tone = { tone } > { status || '—' } < / StatusBadge > ;
2026-03-17 16:23:47 -04:00
}
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
function ThreatLevelBadge ( { level } : { level : string } ) {
const tone =
level === 'high' ? 'error' :
level === 'medium' ? 'warn' :
level === 'low' ? 'pending' :
'inactive' ;
return < StatusBadge tone = { tone } > { level || 'info' } < / StatusBadge > ;
2026-03-17 16:23:47 -04:00
}
// ── Status Tab ────────────────────────────────────────────────────────────────
function StatusTab ( { data , onSync , syncing } : { data : any ; onSync : ( t : string ) = > void ; syncing : boolean } ) {
if ( ! data ) return (
< div className = "flex items-center justify-center py-12" >
< Loader2 className = "w-5 h-5 animate-spin text-muted-foreground" / >
< / div >
) ;
const stats = data . stats ? ? { } ;
return (
< div className = "space-y-6" >
{ /* Connection banner */ }
< div className = "flex items-center justify-between rounded-lg border p-4 bg-muted/30" >
< div className = "space-y-0.5" >
< div className = "flex items-center gap-2" >
{ data . connected
? < CheckCircle2 className = "w-4 h-4 text-green-500" / >
: < XCircle className = "w-4 h-4 text-red-500" / > }
< p className = "text-sm font-medium" >
{ data . connected ? ` Connected — ${ data . accountName ? ? 'Mimecast' } ` : 'Not connected' }
< / p >
{ data . packageName && (
< span className = "text-xs text-muted-foreground" > ( { data . packageName } ) < / span >
) }
< / div >
< p className = "text-xs text-muted-foreground" >
Last sync : { fmtDate ( stats . lastSync ) } · Oldest message : { fmtDate ( stats . oldestMessage ) }
< / p >
< / div >
< div className = "flex gap-2" >
< Button size = "sm" variant = "outline" onClick = { ( ) = > onSync ( 'incremental' ) } disabled = { syncing || ! data . connected } >
{ syncing ? < Loader2 className = "w-4 h-4 animate-spin mr-1" / > : < RefreshCw className = "w-4 h-4 mr-1" / > }
Incremental
< / Button >
< Button size = "sm" onClick = { ( ) = > onSync ( 'full' ) } disabled = { syncing || ! data . connected } >
{ syncing ? < Loader2 className = "w-4 h-4 animate-spin mr-1" / > : < RefreshCw className = "w-4 h-4 mr-1" / > }
Full Sync ( 120 d )
< / Button >
< / div >
< / div >
{ data . error && (
< div className = "rounded-lg border border-red-300 bg-red-50 dark:bg-red-950/20 p-3 text-sm text-red-600" >
{ data . error }
< / div >
) }
{ /* Stats grid */ }
< div className = "grid grid-cols-2 md:grid-cols-5 gap-3" >
< StatCard label = "Total Messages" value = { fmtNum ( stats . messages ) } icon = { Mail } / >
< StatCard label = "Inbound" value = { fmtNum ( stats . inbound ) } icon = { Inbox } cls = "border-blue-500/20 bg-blue-500/5" / >
< StatCard label = "Outbound" value = { fmtNum ( stats . outbound ) } icon = { Send } cls = "border-green-500/20 bg-green-500/5" / >
< StatCard label = "Threat Events" value = { fmtNum ( stats . threats ) } icon = { Shield }
cls = { ( stats . threats ? ? 0 ) > 0 ? 'border-red-500/20 bg-red-500/5' : '' } / >
< StatCard label = "Bodies Stored" value = { fmtNum ( stats . bodies ) } icon = { Activity } / >
< / div >
< div className = "rounded-lg border p-4 space-y-2" >
< p className = "text-xs font-semibold uppercase tracking-wider text-muted-foreground" > Data Retention < / p >
< p className = "text-sm text-muted-foreground" >
120 - day rolling window . Messages older than 120 days are automatically purged on each sync .
Message bodies are fetched for delivered inbound messages ( up to 500 per sync run ) .
< / p >
< / div >
< / div >
) ;
}
// ── Messages Tab ──────────────────────────────────────────────────────────────
function MessagesTab() {
const [ rows , setRows ] = useState < any [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ search , setSearch ] = useState ( '' ) ;
const [ direction , setDirection ] = useState ( '' ) ;
const [ status , setStatus ] = useState ( '' ) ;
const [ days , setDays ] = useState ( '7' ) ;
const load = ( ) = > {
setLoading ( true ) ;
const params = new URLSearchParams ( { days , limit : '200' } ) ;
if ( search ) params . set ( 'search' , search ) ;
if ( direction ) params . set ( 'direction' , direction ) ;
if ( status ) params . set ( 'status' , status ) ;
fetch ( ` /api/mimecast/messages? ${ params } ` )
. then ( r = > r . json ( ) )
. then ( d = > setRows ( d . messages ? ? [ ] ) )
. catch ( ( ) = > setRows ( [ ] ) )
. finally ( ( ) = > setLoading ( false ) ) ;
} ;
useEffect ( ( ) = > { load ( ) ; } , [ days , direction , status ] ) ;
return (
< div className = "space-y-4" >
{ /* Filters */ }
< div className = "flex flex-wrap gap-2" >
< input
type = "text"
placeholder = "Search sender, recipient, subject…"
value = { search }
onChange = { e = > setSearch ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && load ( ) }
className = "flex-1 min-w-48 border rounded-md px-3 py-1.5 text-sm bg-background"
/ >
< select value = { direction } onChange = { e = > setDirection ( e . target . value ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background" >
< option value = "" > All directions < / option >
< option value = "inbound" > Inbound < / option >
< option value = "outbound" > Outbound < / option >
< / select >
< select value = { status } onChange = { e = > setStatus ( e . target . value ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background" >
< option value = "" > All statuses < / option >
< option value = "delivered" > Delivered < / option >
< option value = "rejected" > Rejected < / option >
< option value = "held" > Held < / option >
< option value = "bounced" > Bounced < / option >
< option value = "spam" > Spam < / option >
< / select >
< select value = { days } onChange = { e = > setDays ( e . target . value ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background" >
< option value = "1" > Last 24 h < / option >
< option value = "7" > Last 7 days < / option >
< option value = "30" > Last 30 days < / option >
< option value = "90" > Last 90 days < / option >
< option value = "120" > Last 120 days < / option >
< / select >
< Button size = "sm" variant = "outline" onClick = { load } >
< RefreshCw className = "w-4 h-4 mr-1" / > Search
< / Button >
< / div >
{ loading ? (
< div className = "flex items-center justify-center py-12" >
< Loader2 className = "w-5 h-5 animate-spin text-muted-foreground" / >
< / div >
) : ! rows . length ? (
< div className = "text-center py-12 text-sm text-muted-foreground" >
No messages found — run a sync first or adjust filters
< / div >
) : (
< div className = "rounded-lg border overflow-hidden" >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Table >
< TableHeader className = "bg-muted/50" >
< TableRow >
< TableHead > From < / TableHead >
< TableHead > To < / TableHead >
< TableHead > Subject < / TableHead >
< TableHead > Direction < / TableHead >
< TableHead > Status < / TableHead >
< TableHead > Sent < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
2026-03-17 16:23:47 -04:00
{ rows . map ( ( r : any ) = > (
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< TableRow key = { r . id } >
< TableCell className = "text-xs truncate max-w-[180px]" title = { r . sender_address } > { r . sender_address ? ? '—' } < / TableCell >
< TableCell className = "text-xs truncate max-w-[180px]" title = { r . recipient_address } > { r . recipient_address ? ? '—' } < / TableCell >
< TableCell className = "text-xs truncate max-w-[200px]" title = { r . subject } > { r . subject ? ? '—' } < / TableCell >
< TableCell className = "text-xs capitalize text-muted-foreground" > { r . direction ? ? '—' } < / TableCell >
< TableCell > < MessageStatusBadge status = { r . status } / > < / TableCell >
< TableCell className = "text-xs text-muted-foreground whitespace-nowrap num" > { fmtDate ( r . sent_datetime ) } < / TableCell >
< / TableRow >
2026-03-17 16:23:47 -04:00
) ) }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< / TableBody >
< / Table >
2026-03-17 16:23:47 -04:00
< / div >
) }
< / div >
) ;
}
// ── Threats Tab ───────────────────────────────────────────────────────────────
function ThreatsTab() {
const [ rows , setRows ] = useState < any [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
useEffect ( ( ) = > {
setLoading ( true ) ;
fetch ( '/api/mimecast/threats?limit=200' )
. then ( r = > r . json ( ) )
. then ( d = > setRows ( d . threats ? ? [ ] ) )
. catch ( ( ) = > setRows ( [ ] ) )
. finally ( ( ) = > setLoading ( false ) ) ;
} , [ ] ) ;
if ( loading ) return < div className = "flex items-center justify-center py-12" > < Loader2 className = "w-5 h-5 animate-spin text-muted-foreground" / > < / div > ;
if ( ! rows . length ) return < div className = "text-center py-12 text-sm text-muted-foreground" > No threat events — run a sync first < / div > ;
return (
< div className = "rounded-lg border overflow-hidden" >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Table >
< TableHeader className = "bg-muted/50" >
< TableRow >
< TableHead > Type < / TableHead >
< TableHead > Level < / TableHead >
< TableHead > Actor < / TableHead >
< TableHead > Verdict < / TableHead >
< TableHead > URL / File < / TableHead >
< TableHead > When < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
2026-03-17 16:23:47 -04:00
{ rows . map ( ( r : any ) = > (
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< TableRow key = { r . id } >
< TableCell className = "text-xs capitalize" > { r . event_type ? ? '—' } < / TableCell >
< TableCell > < ThreatLevelBadge level = { r . threat_level } / > < / TableCell >
< TableCell className = "text-xs text-muted-foreground" > { r . actor_email ? ? '—' } < / TableCell >
< TableCell className = "text-xs text-muted-foreground capitalize" > { r . verdict ? ? '—' } < / TableCell >
< TableCell className = "text-xs text-muted-foreground truncate max-w-[200px]" title = { r . url ? ? r . file_name ? ? '' } >
2026-03-17 16:23:47 -04:00
{ r . url ? ? r . file_name ? ? '—' }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< / TableCell >
< TableCell className = "text-xs text-muted-foreground whitespace-nowrap num" > { fmtDate ( r . event_datetime ) } < / TableCell >
< / TableRow >
2026-03-17 16:23:47 -04:00
) ) }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< / TableBody >
< / Table >
2026-03-17 16:23:47 -04:00
< / div >
) ;
}
2026-03-31 22:38:22 -04:00
// ── Cloud Users Tab ───────────────────────────────────────────────────────────
function CloudUserTab() {
const [ email , setEmail ] = useState ( '' ) ;
const [ domain , setDomain ] = useState ( '' ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ result , setResult ] = useState < any > ( null ) ;
const [ showRaw , setShowRaw ] = useState ( false ) ;
const handleEmailChange = ( v : string ) = > {
setEmail ( v ) ;
const atIdx = v . indexOf ( '@' ) ;
if ( atIdx >= 0 ) setDomain ( v . slice ( atIdx + 1 ) ) ;
} ;
const lookup = async ( ) = > {
if ( ! email || ! domain ) return ;
setLoading ( true ) ;
setResult ( null ) ;
setShowRaw ( false ) ;
try {
const params = new URLSearchParams ( { emailAddress : email , domain } ) ;
const res = await fetch ( ` /api/mimecast/cloud-user? ${ params } ` ) ;
setResult ( await res . json ( ) ) ;
} catch ( err : any ) {
setResult ( { error : err.message } ) ;
} finally {
setLoading ( false ) ;
}
} ;
const user = result ? . user ;
const lockedOut = user ? . lockedOut ? ? false ;
return (
< div className = "space-y-5" >
< div className = "flex flex-wrap gap-2 items-end" >
< div className = "flex flex-col gap-1" >
< label className = "text-xs text-muted-foreground" > Email address < / label >
< input
type = "email"
placeholder = "user@domain.com"
value = { email }
onChange = { e = > handleEmailChange ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && lookup ( ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background w-72"
/ >
< / div >
< div className = "flex flex-col gap-1" >
< label className = "text-xs text-muted-foreground" > Domain < / label >
< input
type = "text"
placeholder = "domain.com"
value = { domain }
onChange = { e = > setDomain ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && lookup ( ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background w-48"
/ >
< / div >
< Button size = "sm" onClick = { lookup } disabled = { loading || ! email || ! domain } >
{ loading ? < Loader2 className = "w-4 h-4 animate-spin mr-1" / > : < Search className = "w-4 h-4 mr-1" / > }
Look Up
< / Button >
< / div >
{ result ? . error && (
< div className = "rounded-lg border border-red-300 bg-red-50 dark:bg-red-950/20 p-3 text-sm text-red-600" >
{ result . error }
< / div >
) }
{ result && ! result . error && ! result . found && (
< div className = "rounded-lg border p-4 text-sm text-muted-foreground" >
User not found in Mimecast Cloud Gateway .
< / div >
) }
{ user && (
< div className = "space-y-3" >
< div className = { ` rounded-lg border p-4 flex items-center gap-3 ${ lockedOut ? 'border-red-400 bg-red-50 dark:bg-red-950/20' : 'border-green-400 bg-green-50 dark:bg-green-950/20' } ` } >
{ lockedOut
? < LockKeyhole className = "w-5 h-5 text-red-600 shrink-0" / >
: < UnlockKeyhole className = "w-5 h-5 text-green-600 shrink-0" / > }
< div >
< p className = { ` font-semibold text-sm ${ lockedOut ? 'text-red-700' : 'text-green-700' } ` } >
{ lockedOut ? 'Account Locked Out' : 'Account Active' }
< / p >
< p className = "text-xs text-muted-foreground" >
{ user . name && < span > { user . name } · < / span > }
{ user . emailAddress }
{ user . status && < span > · Status : { user . status } < / span > }
< / p >
< / div >
< / div >
< button
className = "text-xs text-muted-foreground underline-offset-2 hover:underline"
onClick = { ( ) = > setShowRaw ( v = > ! v ) }
>
{ showRaw ? 'Hide' : 'Show' } raw response
< / button >
{ showRaw && (
< pre className = "rounded-lg border bg-muted/30 p-3 text-xs overflow-auto max-h-72" >
{ JSON . stringify ( user . _raw ? ? user , null , 2 ) }
< / pre >
) }
< / div >
) }
< / div >
) ;
}
2026-03-17 16:23:47 -04:00
// ── History Tab ───────────────────────────────────────────────────────────────
function HistoryTab() {
const [ rows , setRows ] = useState < any [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
useEffect ( ( ) = > {
setLoading ( true ) ;
fetch ( '/api/sync/history?entityType=mimecast&limit=30' )
. then ( r = > r . json ( ) )
. then ( d = > setRows ( d . history ? ? [ ] ) )
. catch ( ( ) = > setRows ( [ ] ) )
. finally ( ( ) = > setLoading ( false ) ) ;
} , [ ] ) ;
if ( loading ) return < div className = "flex items-center justify-center py-12" > < Loader2 className = "w-5 h-5 animate-spin text-muted-foreground" / > < / div > ;
if ( ! rows . length ) return < div className = "text-center py-12 text-sm text-muted-foreground" > No sync history yet < / div > ;
return (
< div className = "rounded-lg border overflow-hidden" >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Table >
< TableHeader className = "bg-muted/50" >
< TableRow >
< TableHead > Type < / TableHead >
< TableHead > Status < / TableHead >
< TableHead > Messages < / TableHead >
< TableHead > Threats < / TableHead >
< TableHead > Started < / TableHead >
< TableHead > Duration < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
2026-03-17 16:23:47 -04:00
{ rows . map ( ( r : any , i : number ) = > {
const dur = r . completed_at && r . started_at
? new Date ( r . completed_at ) . getTime ( ) - new Date ( r . started_at ) . getTime ( )
: null ;
const durStr = dur == null ? '—' : dur < 60000 ? ` ${ Math . round ( dur / 1000 ) } s ` : ` ${ Math . floor ( dur / 60000 ) } m ${ Math . round ( ( dur % 60000 ) / 1000 ) } s ` ;
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
const statusTone =
r . status === 'completed' ? 'ok' :
r . status === 'failed' ? 'error' :
'inactive' ;
2026-03-17 16:23:47 -04:00
const meta = typeof r . metadata === 'string' ? JSON . parse ( r . metadata || '{}' ) : ( r . metadata ? ? { } ) ;
return (
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< TableRow key = { i } >
< TableCell className = "capitalize text-xs" > { r . sync_type ? ? '—' } < / TableCell >
< TableCell >
< StatusBadge tone = { statusTone } > { r . status } < / StatusBadge >
< / TableCell >
< TableCell className = "num text-xs" > { fmtNum ( meta . messagesUpserted ? ? r . records_added ) } < / TableCell >
< TableCell className = "num text-xs" > { fmtNum ( meta . threatsUpserted ) } < / TableCell >
< TableCell className = "text-xs text-muted-foreground num" > { fmtDate ( r . started_at ) } < / TableCell >
< TableCell className = "text-xs text-muted-foreground num" > { durStr } < / TableCell >
< / TableRow >
2026-03-17 16:23:47 -04:00
) ;
} ) }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< / TableBody >
< / Table >
2026-03-17 16:23:47 -04:00
< / div >
) ;
}
2026-04-01 07:18:52 -04:00
// ── Message Analysis ─────────────────────────────────────────────────────────
2026-04-01 07:32:09 -04:00
type AnalysisAction = { label : string ; description : string ; type : 'release' | 'info' ; warning? : boolean } ;
type Analysis = {
headline : string ;
explanation : string ;
severity : 'high' | 'medium' | 'low' ;
priorSteps : string [ ] ;
actions : AnalysisAction [ ] ;
} ;
function analyzeMessage ( m : any ) : Analysis {
const code : string = ( m . reasonCode ? ? '' ) . toLowerCase ( ) ;
const policy : string = ( m . policyInfo ? ? '' ) . toLowerCase ( ) ;
2026-04-01 07:18:52 -04:00
const reason : string = ( m . reason ? ? '' ) . toLowerCase ( ) ;
const from : string = m . from ? ? '' ;
const fromDisplay : string = m . fromDisplay ? ? '' ;
2026-04-01 07:32:09 -04:00
const fromDomain = from . includes ( '@' ) ? from . split ( '@' ) [ 1 ] : from ;
const subject : string = ( m . subject ? ? '' ) . toLowerCase ( ) ;
// What Mimecast has already evaluated (always true — it went through the full pipeline)
const priorSteps = [
'Passed through Mimecast inbound gateway' ,
'Evaluated against permitted sender policies — no matching bypass found' ,
'Evaluated against recipient-based allow rules — no match' ,
] ;
// DMARC
if ( code . includes ( 'dmarc' ) || policy . includes ( 'dmarc' ) || reason . includes ( 'dmarc' ) ) {
2026-04-01 07:18:52 -04:00
return {
headline : 'DMARC Authentication Failure' ,
2026-04-01 07:32:09 -04:00
severity : 'high' ,
explanation : ` The sending domain failed DMARC validation. The "From" address ( ${ fromDisplay || from } ) does not align with the domain that actually sent the message (SPF/DKIM mismatch). This can indicate spoofing — but also fires for legitimate senders using shared email infrastructure (e.g. Mailchimp, Zendesk, HubSpot) who haven't set up DKIM alignment. ` ,
priorSteps : [ . . . priorSteps , 'SPF and DKIM alignment checks failed' ] ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Release this message' , description : 'Deliver it now if you recognise the sender. The recipient will receive it normally.' , type : 'release' } ,
{ label : 'Add a permitted sender policy' , description : ` In Mimecast: Administration > Gateway > Policies > Permitted Senders. Add sender domain " ${ fromDomain } " to bypass DMARC holds for this domain going forward. ` , type : 'info' } ,
{ label : 'Ask the sender to fix their authentication' , description : 'The sender should configure DKIM signing on their email platform and ensure the d= domain in DKIM matches their From domain.' , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
// Impersonation
2026-04-01 07:32:09 -04:00
if ( code . includes ( 'impersonation' ) || policy . includes ( 'impersonation' ) || reason . includes ( 'impersonation' ) ) {
2026-04-01 07:18:52 -04:00
return {
2026-04-01 07:32:09 -04:00
headline : 'Impersonation Protection Hold' ,
severity : 'high' ,
explanation : ` Mimecast's impersonation protection flagged " ${ fromDisplay || from } " as potentially impersonating an internal user or trusted contact. The display name may match an executive or employee while the sending address is external. This is the primary vector for BEC (Business Email Compromise) fraud. ` ,
priorSteps : [ . . . priorSteps , 'Display name matched internal user list — external sender flagged' ] ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Release this message' , description : 'Only release after verifying identity through another channel (phone/Teams). Do not confirm via reply to the held email.' , type : 'release' , warning : true } ,
{ label : 'Add to permitted senders' , description : ` If this is a legitimate contact, add " ${ from } " as a permitted sender in Mimecast to bypass impersonation checks for this specific address. ` , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
2026-04-01 07:32:09 -04:00
// Spam
if ( code . includes ( 'spam' ) || policy . includes ( 'spam' ) || reason . includes ( 'spam' ) ) {
const isAuthCode = subject . includes ( 'authentication code' ) || subject . includes ( 'verification code' ) ||
subject . includes ( 'your code' ) || subject . includes ( 'otp' ) || subject . includes ( 'one-time' ) ||
subject . includes ( 'access code' ) || subject . includes ( 'login code' ) ;
const isMarketing = subject . includes ( 'unsubscribe' ) || subject . includes ( 'offer' ) ||
subject . includes ( 'deal' ) || subject . includes ( 'sale' ) || subject . includes ( 'newsletter' ) ||
( m . hasAttachments === false && m . size > 30000 ) ;
if ( isAuthCode ) {
return {
headline : 'Authentication Code — Held by Spam Filter' ,
severity : 'low' ,
explanation : ` This is almost certainly a legitimate authentication or verification code email from " ${ fromDisplay || from } ". It was caught by spam detection due to the sending infrastructure's reputation score — not because the content is malicious. The recipient is likely waiting for this code. ` ,
priorSteps : [ . . . priorSteps , ` Spam score exceeded threshold for policy " ${ m . policyInfo } " ` , 'No permitted sender rule found for this address' ] ,
actions : [
{ label : 'Release this message' , description : 'Deliver it now. The verification code is time-sensitive.' , type : 'release' } ,
{ label : 'Add permitted sender rule' , description : ` Add " ${ from } " to Mimecast > Administration > Gateway > Policies > Permitted Senders so future codes from this address are delivered without holds. ` , type : 'info' } ,
] ,
} ;
}
if ( isMarketing ) {
2026-04-01 07:18:52 -04:00
return {
2026-04-01 07:32:09 -04:00
headline : 'Marketing / Promotional Email' ,
severity : 'low' ,
explanation : ` This appears to be a marketing or promotional email from " ${ fromDisplay || from } " that triggered the spam policy " ${ m . policyInfo } ". These are frequently held when sent from bulk mail platforms (Mailchimp, Constant Contact, etc.) with mixed sender reputation. ` ,
priorSteps : [ . . . priorSteps , ` Spam score exceeded threshold for policy " ${ m . policyInfo } " ` ] ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Release this message' , description : 'Deliver if the recipient has opted in or is expecting communications from this sender.' , type : 'release' } ,
{ label : 'Add permitted sender rule' , description : ` Add sender domain " ${ fromDomain } " to permitted senders if this is a trusted marketing partner. ` , type : 'info' } ,
{ label : 'Block this sender' , description : ` Add " ${ fromDomain } " to blocked senders in Mimecast > Gateway > Policies > Blocked Senders if this is unwanted mail. ` , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
return {
2026-04-01 07:32:09 -04:00
headline : 'Spam Signature Match' ,
severity : 'medium' ,
explanation : ` The email from " ${ fromDisplay || from } " matched a spam signature under policy " ${ m . policyInfo } ". This can be a false positive for legitimate transactional or notification emails sent through shared infrastructure with a low sender reputation. ` ,
priorSteps : [ . . . priorSteps , ` Spam score exceeded threshold for policy " ${ m . policyInfo } " ` ] ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Release this message' , description : 'Deliver if you recognise the sender and the recipient is expecting this email.' , type : 'release' } ,
{ label : 'Add permitted sender rule' , description : ` Add " ${ from } " to Mimecast > Administration > Gateway > Policies > Permitted Senders to prevent future holds. ` , type : 'info' } ,
{ label : 'Block this sender' , description : ` Add " ${ fromDomain } " to blocked senders if this is definitively spam. ` , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
2026-04-01 07:32:09 -04:00
// Malware / threat
if ( code . includes ( 'malware' ) || code . includes ( 'virus' ) || code . includes ( 'threat' ) ||
policy . includes ( 'malware' ) || reason . includes ( 'malware' ) ) {
2026-04-01 07:18:52 -04:00
return {
2026-04-01 07:32:09 -04:00
headline : 'Malware or Threat Detected' ,
severity : 'high' ,
explanation : ` Mimecast detected a potentially malicious attachment or URL in this email. Do not release without thorough review by a security administrator. ` ,
priorSteps : [ . . . priorSteps , 'Attachment/URL scanned — threat signature matched' ] ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Do not release without security review' , description : 'Contact the sender through a separate channel to verify this email is legitimate before considering release.' , type : 'info' , warning : true } ,
{ label : 'View full threat details' , description : 'Open Mimecast Administration Console > Gateway > Held Queue to view the full attachment analysis and URL scan results.' , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
return {
headline : 'Message Hold Applied' ,
2026-04-01 07:32:09 -04:00
severity : 'medium' ,
explanation : ` This email was held under policy " ${ m . policyInfo || m . reason } ". Review the sender and content before releasing. ` ,
priorSteps ,
2026-04-01 07:18:52 -04:00
actions : [
2026-04-01 07:32:09 -04:00
{ label : 'Release this message' , description : 'Deliver it to the recipient if you determine it is safe.' , type : 'release' } ,
{ label : 'Add permitted sender rule' , description : ` Add " ${ from } " to Mimecast > Administration > Gateway > Policies > Permitted Senders to bypass this hold for future messages. ` , type : 'info' } ,
2026-04-01 07:18:52 -04:00
] ,
} ;
}
function MessageAnalysisDialog ( { message , onClose , onRelease , releasing } : {
message : any ;
onClose : ( ) = > void ;
onRelease : ( m : any ) = > void ;
releasing : boolean ;
} ) {
if ( ! message ) return null ;
const analysis = analyzeMessage ( message ) ;
2026-04-01 07:32:09 -04:00
const severityBar = {
high : 'border-l-red-500' ,
medium : 'border-l-amber-500' ,
low : 'border-l-blue-500' ,
} ;
const severityBadge = {
high : 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-400' ,
medium : 'bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400' ,
low : 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400' ,
2026-04-01 07:18:52 -04:00
} ;
2026-04-01 07:32:09 -04:00
const severityLabel = { high : 'High risk' , medium : 'Review needed' , low : 'Likely safe' } ;
2026-04-01 07:18:52 -04:00
const severityIcon = {
2026-04-01 07:32:09 -04:00
high : < XCircle className = "w-4 h-4" / > ,
medium : < AlertTriangle className = "w-4 h-4" / > ,
low : < CheckCircle2 className = "w-4 h-4" / > ,
2026-04-01 07:18:52 -04:00
} ;
return (
< Dialog open = { ! ! message } onOpenChange = { open = > ! open && onClose ( ) } >
2026-04-01 07:32:09 -04:00
< DialogContent className = "max-w-xl w-full max-h-[85vh] overflow-y-auto" >
2026-04-01 07:18:52 -04:00
< DialogHeader >
2026-04-01 07:32:09 -04:00
< div className = "flex items-center gap-2 flex-wrap" >
< DialogTitle className = "text-base" > { analysis . headline } < / DialogTitle >
< span className = { ` inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${ severityBadge [ analysis . severity ] } ` } >
{ severityIcon [ analysis . severity ] }
{ severityLabel [ analysis . severity ] }
< / span >
< / div >
2026-04-01 07:18:52 -04:00
< / DialogHeader >
{ /* Message details */ }
2026-04-01 07:32:09 -04:00
< div className = "rounded-md border bg-muted/20 divide-y text-sm" >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Subject < / span >
< span className = "font-medium break-words" > { message . subject || '(no subject)' } < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > From < / span >
< span className = "break-all" > { message . fromDisplay ? ` ${ message . fromDisplay } ` : '' } < span className = "text-muted-foreground" > { message . fromDisplay ? ` < ${ message . from } > ` : message . from } < / span > < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > To < / span >
< span > { message . toDisplay || message . to } < / span >
2026-04-01 07:18:52 -04:00
< / div >
2026-04-01 07:32:09 -04:00
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Received < / span >
< span > { new Date ( message . dateReceived ) . toLocaleString ( ) } < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Policy < / span >
< span className = "font-medium" > { message . policyInfo || '—' } < / span >
< / div >
{ message . reason && (
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Reason < / span >
< span className = "text-muted-foreground" > { message . reason } < / span >
< / div >
) }
2026-04-01 07:40:49 -04:00
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Size < / span >
< span className = "text-muted-foreground" >
{ message . size ? ` ${ ( message . size / 1024 ) . toFixed ( 1 ) } KB ` : '—' }
{ message . hasAttachments ? < span className = "ml-2 inline-flex items-center gap-0.5 text-xs" > < Mail className = "w-3 h-3" / > has attachments < / span > : '' }
< / span >
< / div >
< / div >
{ /* Body not available note */ }
< div className = "rounded-md border border-dashed px-3 py-2.5 text-xs text-muted-foreground flex items-start gap-2" >
< Info className = "w-3.5 h-3.5 mt-0.5 flex-shrink-0" / >
< span > Message body is not accessible via the Mimecast held mail API . To preview the full content , open the Mimecast Administration Console .
< a href = "https://admin.services.mimecast.com" target = "_blank" rel = "noopener noreferrer" className = "ml-1 underline hover:text-foreground" > Open Mimecast Console → < / a >
< / span >
2026-04-01 07:18:52 -04:00
< / div >
{ /* Explanation */ }
2026-04-01 07:32:09 -04:00
< div className = { ` rounded-md border-l-4 border border-border pl-3 pr-3 py-2.5 text-sm text-foreground/90 ${ severityBar [ analysis . severity ] } ` } >
2026-04-01 07:18:52 -04:00
{ analysis . explanation }
< / div >
2026-04-01 07:32:09 -04:00
{ /* What Mimecast already checked */ }
< div className = "space-y-1.5" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > What was already evaluated < / p >
< div className = "rounded-md border bg-muted/10 divide-y" >
{ analysis . priorSteps . map ( ( step , i ) = > (
< div key = { i } className = "flex items-start gap-2 px-3 py-2 text-xs text-muted-foreground" >
< CheckCircle2 className = "w-3.5 h-3.5 mt-0.5 flex-shrink-0 text-muted-foreground/50" / >
{ step }
< / div >
) ) }
< / div >
< / div >
2026-04-01 07:18:52 -04:00
{ /* Resolution options */ }
2026-04-01 07:32:09 -04:00
< div className = "space-y-1.5" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > Resolution options < / p >
< div className = "space-y-2" >
{ analysis . actions . map ( ( action , i ) = > (
< div key = { i } className = { ` rounded-md border p-3 flex items-start justify-between gap-3 ${
action . warning ? 'border-amber-300 bg-amber-50/50 dark:bg-amber-950/10' : 'bg-background'
} ` }>
< div className = "space-y-0.5 flex-1 min-w-0" >
< p className = "text-sm font-medium" > { action . label } < / p >
< p className = "text-xs text-muted-foreground" > { action . description } < / p >
< / div >
{ action . type === 'release' && (
< Button
size = "sm"
variant = "outline"
2026-04-01 07:40:49 -04:00
className = "flex-shrink-0 text-green-700 border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
2026-04-01 07:32:09 -04:00
disabled = { releasing }
onClick = { ( ) = > { onRelease ( message ) ; onClose ( ) ; } }
>
2026-04-01 07:40:49 -04:00
{ releasing && < Loader2 className = "w-3 h-3 animate-spin mr-1" / > }
2026-04-01 07:32:09 -04:00
Release
< / Button >
) }
2026-04-01 07:18:52 -04:00
< / div >
2026-04-01 07:32:09 -04:00
) ) }
< / div >
2026-04-01 07:18:52 -04:00
< / div >
< / DialogContent >
< / Dialog >
) ;
}
2026-03-31 22:38:22 -04:00
// ── Held Mail Tab ─────────────────────────────────────────────────────────────
2026-04-01 07:09:42 -04:00
const TENANT_OPTIONS = [
{ id : '1' , name : 'Wulf Consulting' } ,
{ id : '2' , name : 'Seubert & Associates' } ,
] ;
2026-03-31 22:38:22 -04:00
function HeldMailTab() {
const [ data , setData ] = useState < any > ( null ) ;
2026-04-01 07:09:42 -04:00
const [ messages , setMessages ] = useState < any [ ] > ( [ ] ) ;
2026-03-31 22:38:22 -04:00
const [ loading , setLoading ] = useState ( false ) ;
2026-04-01 06:52:58 -04:00
const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
2026-03-31 22:38:22 -04:00
const [ recipient , setRecipient ] = useState ( '' ) ;
2026-04-01 07:09:42 -04:00
const [ tenantId , setTenantId ] = useState ( '1' ) ;
2026-03-31 22:38:22 -04:00
const [ policyFilter , setPolicyFilter ] = useState ( '' ) ;
const [ loaded , setLoaded ] = useState ( false ) ;
2026-04-01 07:09:42 -04:00
const [ releasing , setReleasing ] = useState < Record < string , boolean > > ( { } ) ;
const [ releaseErrors , setReleaseErrors ] = useState < Record < string , string > > ( { } ) ;
2026-04-01 07:18:52 -04:00
const [ analysisMessage , setAnalysisMessage ] = useState < any > ( null ) ;
2026-03-31 22:38:22 -04:00
2026-04-01 06:52:58 -04:00
const load = async ( recipientVal? : string ) = > {
2026-03-31 22:38:22 -04:00
setLoading ( true ) ;
2026-04-01 06:52:58 -04:00
setLoadError ( null ) ;
2026-03-31 22:38:22 -04:00
const params = new URLSearchParams ( ) ;
const r = recipientVal ? ? recipient ;
if ( r ) params . set ( 'recipient' , r ) ;
2026-04-01 07:09:42 -04:00
if ( tenantId ) params . set ( 'tenantId' , tenantId ) ;
2026-04-01 06:52:58 -04:00
try {
const res = await fetch ( ` /api/mimecast/held? ${ params } ` ) ;
if ( ! res . ok ) {
const text = await res . text ( ) ;
throw new Error ( ` HTTP ${ res . status } : ${ text . slice ( 0 , 200 ) } ` ) ;
}
const d = await res . json ( ) ;
setData ( d ) ;
2026-04-01 07:09:42 -04:00
setMessages ( d . messages ? ? [ ] ) ;
2026-04-01 06:52:58 -04:00
setLoaded ( true ) ;
} catch ( e : any ) {
setLoadError ( e . message ? ? 'Unknown error' ) ;
} finally {
setLoading ( false ) ;
}
2026-03-31 22:38:22 -04:00
} ;
2026-04-01 07:09:42 -04:00
const release = async ( m : any ) = > {
setReleasing ( r = > ( { . . . r , [ m . id ] : true } ) ) ;
setReleaseErrors ( e = > { const n = { . . . e } ; delete n [ m . id ] ; return n ; } ) ;
try {
const res = await fetch ( '/api/mimecast/held/release' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { id : m.id , tenantId : m.tenantId } ) ,
} ) ;
const d = await res . json ( ) ;
if ( ! res . ok || ! d . released ) {
throw new Error ( d . error ? ? 'Release failed' ) ;
}
// Optimistically remove from list
setMessages ( prev = > prev . filter ( x = > x . id !== m . id ) ) ;
} catch ( e : any ) {
setReleaseErrors ( prev = > ( { . . . prev , [ m . id ] : e . message } ) ) ;
} finally {
setReleasing ( r = > { const n = { . . . r } ; delete n [ m . id ] ; return n ; } ) ;
}
} ;
2026-04-01 08:08:59 -04:00
const recipientLower = recipient . trim ( ) . toLowerCase ( ) ;
const filtered = messages . filter ( m = > {
if ( recipientLower && ! m . to ? . toLowerCase ( ) . includes ( recipientLower ) && ! m . toDisplay ? . toLowerCase ( ) . includes ( recipientLower ) ) return false ;
if ( policyFilter && ! m . policyInfo ? . toLowerCase ( ) . includes ( policyFilter . toLowerCase ( ) ) ) return false ;
return true ;
} ) ;
2026-03-31 22:38:22 -04:00
const policies = [ . . . new Set ( messages . map ( ( m : any ) = > m . policyInfo ) . filter ( Boolean ) ) ] . sort ( ) ;
2026-04-01 07:09:42 -04:00
const tenantInfo : any [ ] = data ? . tenants ? ? [ ] ;
const currentTenant = tenantInfo [ 0 ] ;
2026-03-31 22:38:22 -04:00
return (
< div className = "space-y-4" >
2026-04-01 07:09:42 -04:00
{ /* Controls */ }
2026-03-31 22:38:22 -04:00
< div className = "flex flex-wrap gap-2 items-end" >
2026-04-01 07:09:42 -04:00
< div >
< label className = "text-xs text-muted-foreground mb-1 block" > Tenant < / label >
< select
value = { tenantId }
onChange = { e = > { setTenantId ( e . target . value ) ; setLoaded ( false ) ; setData ( null ) ; setMessages ( [ ] ) ; } }
className = "border rounded-md px-3 py-1.5 text-sm bg-background"
>
{ TENANT_OPTIONS . map ( t = > (
< option key = { t . id } value = { t . id } > { t . name } < / option >
) ) }
< / select >
< / div >
2026-03-31 22:38:22 -04:00
< div className = "flex-1 min-w-56" >
2026-04-01 08:08:59 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" >
Recipient email
{ loaded && recipient . trim ( ) && (
< span className = "ml-1 text-muted-foreground/60" > ( { filtered . length } match { filtered . length !== 1 ? 'es' : '' } ) < / span >
) }
< / label >
2026-03-31 22:38:22 -04:00
< input
type = "text"
2026-04-01 07:09:42 -04:00
placeholder = "filter by recipient…"
2026-03-31 22:38:22 -04:00
value = { recipient }
onChange = { e = > setRecipient ( e . target . value ) }
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background"
/ >
< / div >
{ loaded && policies . length > 0 && (
< div >
< label className = "text-xs text-muted-foreground mb-1 block" > Policy < / label >
< select value = { policyFilter } onChange = { e = > setPolicyFilter ( e . target . value ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background min-w-48" >
< option value = "" > All policies < / option >
{ policies . map ( p = > < option key = { p } value = { p } > { p } < / option > ) }
< / select >
< / div >
) }
< Button onClick = { ( ) = > load ( ) } disabled = { loading } className = "gap-2" >
{ loading ? < Loader2 className = "w-4 h-4 animate-spin" / > : < Search className = "w-4 h-4" / > }
{ loaded ? 'Refresh' : 'Load Held Mail' }
< / Button >
< / div >
2026-04-01 07:09:42 -04:00
{ /* Tenant summary badge */ }
{ loaded && currentTenant && (
2026-03-31 22:38:22 -04:00
< div className = "flex flex-wrap gap-2" >
2026-04-01 07:09:42 -04:00
< div className = { ` flex items-center gap-1.5 rounded-full px-3 py-1 text-xs border ${
currentTenant . error ? 'border-red-300 bg-red-50 dark:bg-red-950/20 text-red-600' :
currentTenant . count > 0 ? 'border-yellow-400/40 bg-yellow-500/5 text-yellow-700' :
'border-border bg-muted/30 text-muted-foreground'
} ` }>
< Building2 className = "w-3 h-3" / >
< span className = "font-medium" > { currentTenant . accountName } < / span >
{ currentTenant . error
? < span title = { currentTenant . error } > — permission denied < / span >
: < span > — showing { messages . length . toLocaleString ( ) } { currentTenant . totalCount > messages . length ? ` of ${ currentTenant . totalCount . toLocaleString ( ) } ` : '' } held < / span >
}
< / div >
2026-03-31 22:38:22 -04:00
< / div >
) }
2026-04-01 06:52:58 -04:00
{ loadError && (
< div className = "rounded-lg border border-red-300 bg-red-50 dark:bg-red-950/20 p-3 text-sm text-red-600" >
{ loadError }
< / div >
) }
{ ! loaded && ! loading && ! loadError && (
2026-03-31 22:38:22 -04:00
< div className = "rounded-lg border border-dashed p-12 text-center text-muted-foreground text-sm" >
2026-04-01 07:09:42 -04:00
Select a tenant and click & ldquo ; Load Held Mail & rdquo ;
2026-03-31 22:38:22 -04:00
< / div >
) }
{ loading && (
< div className = "flex items-center justify-center py-16" >
< Loader2 className = "w-6 h-6 animate-spin text-muted-foreground" / >
2026-04-01 07:09:42 -04:00
< span className = "ml-2 text-sm text-muted-foreground" > Fetching held messages … < / span >
2026-03-31 22:38:22 -04:00
< / div >
) }
{ loaded && ! loading && filtered . length === 0 && (
< div className = "rounded-lg border p-12 text-center text-muted-foreground text-sm" >
No held messages found .
< / div >
) }
{ loaded && ! loading && filtered . length > 0 && (
< div className = "rounded-lg border overflow-hidden" >
2026-04-01 07:09:42 -04:00
< div className = "px-4 py-2 bg-muted/40 border-b" >
2026-03-31 22:38:22 -04:00
< span className = "text-sm font-medium" >
{ filtered . length . toLocaleString ( ) } held message { filtered . length !== 1 ? 's' : '' }
2026-04-01 08:11:03 -04:00
{ filtered . length < messages . length
? ` — filtered from ${ messages . length . toLocaleString ( ) } ${ currentTenant ? . totalCount > messages . length ? ` of ${ currentTenant . totalCount . toLocaleString ( ) } total ` : '' } `
: currentTenant ? . totalCount > messages . length
? ` (showing ${ messages . length . toLocaleString ( ) } of ${ currentTenant . totalCount . toLocaleString ( ) } total) `
: '' }
2026-03-31 22:38:22 -04:00
< / span >
< / div >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Table className = "table-fixed" >
< TableHeader className = "bg-muted/30" >
< TableRow >
< TableHead style = { { width : '120px' } } className = "text-xs" > Date < / TableHead >
< TableHead style = { { width : '160px' } } className = "text-xs" > To < / TableHead >
< TableHead style = { { width : '180px' } } className = "text-xs" > From < / TableHead >
< TableHead className = "text-xs" > Subject < / TableHead >
< TableHead style = { { width : '160px' } } className = "text-xs" > Policy < / TableHead >
< TableHead style = { { width : '160px' } } > < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
{ filtered . map ( ( m : any ) = > (
< TableRow key = { m . id } >
< TableCell className = "text-muted-foreground whitespace-nowrap text-xs num" >
{ new Date ( m . dateReceived ) . toLocaleString ( undefined , { month : 'short' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) }
< / TableCell >
< TableCell className = "text-xs" style = { { overflow : 'hidden' } } >
< div className = "truncate" > { m . to } < / div >
< / TableCell >
< TableCell style = { { overflow : 'hidden' } } >
< div className = "font-medium text-xs truncate" > { m . fromDisplay || m . from } < / div >
{ m . fromDisplay && < div className = "text-xs text-muted-foreground truncate" > { m . from } < / div > }
< / TableCell >
< TableCell className = "text-xs" style = { { overflow : 'hidden' } } >
< div className = "truncate" > { m . subject || '(no subject)' } < / div >
< / TableCell >
< TableCell style = { { overflow : 'hidden' } } >
< StatusBadge
tone = {
2026-03-31 22:38:22 -04:00
m . policyInfo ? . includes ( 'DMARC' ) || m . policyInfo ? . includes ( 'Impersonation' )
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
? 'error'
: 'inactive'
}
>
{ m . policyInfo || m . reason || '—' }
< / StatusBadge >
< / TableCell >
< TableCell >
< div className = "flex items-center gap-1 justify-end" >
< Button
size = "sm"
variant = "ghost"
className = "h-7 text-xs px-2 whitespace-nowrap"
onClick = { ( ) = > setAnalysisMessage ( m ) }
>
< Info className = "w-3 h-3 mr-1" / >
Analyze
< / Button >
< Button
size = "sm"
variant = "outline"
className = "h-7 text-xs px-2 whitespace-nowrap text-green-700 border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
disabled = { releasing [ m . id ] }
onClick = { ( ) = > release ( m ) }
>
{ releasing [ m . id ] ? < Loader2 className = "w-3 h-3 animate-spin mr-1" / > : null }
Release
< / Button >
< / div >
{ releaseErrors [ m . id ] && (
< div className = "text-xs text-red-500 text-right mt-0.5" > { releaseErrors [ m . id ] } < / div >
) }
< / TableCell >
< / TableRow >
) ) }
< / TableBody >
< / Table >
2026-03-31 22:38:22 -04:00
< / div >
) }
2026-04-01 07:18:52 -04:00
< MessageAnalysisDialog
message = { analysisMessage }
onClose = { ( ) = > setAnalysisMessage ( null ) }
onRelease = { release }
releasing = { analysisMessage ? ! ! releasing [ analysisMessage . id ] : false }
/ >
2026-03-31 22:38:22 -04:00
< / div >
) ;
}
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
// ── Delivered Mail Tab ────────────────────────────────────────────────────────
2026-04-01 09:42:37 -04:00
// Known phishing/scam subject patterns
const SEXTORTION_PATTERNS = [
/you pervert/i , /i recorded you/i , /i have your password/i , /i hacked your/i ,
/your device was hacked/i , /your camera was/i , /rat (software|trojan)/i ,
/pay .{0,20}bitcoin/i , /send .{0,20}btc/i , /your (intimate|private|sexual) (video|footage|content)/i ,
/i have access to your/i , /you visited (adult|porn|xxx)/i ,
] ;
const PHISHING_PATTERNS = [
/verify your account/i , /your account (has been|will be) (suspended|terminated|closed|locked)/i ,
/click here to (verify|confirm|restore|unlock|reactivate)/i ,
/unusual (sign|login|activity) (in|on|detected)/i ,
/update your (billing|payment|credit card) (info|information|details)/i ,
/you have (won|been selected|been chosen)/i ,
/claim your (prize|reward|gift card)/i ,
/wire transfer/i , /urgent (action|response) (required|needed)/i ,
/your (order|package|parcel|shipment) (is|has been) (held|delayed|pending)/i ,
] ;
function detectSubjectThreat ( subject : string ) : 'sextortion' | 'phishing' | null {
const s = subject ? ? '' ;
if ( SEXTORTION_PATTERNS . some ( p = > p . test ( s ) ) ) return 'sextortion' ;
if ( PHISHING_PATTERNS . some ( p = > p . test ( s ) ) ) return 'phishing' ;
return null ;
}
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
function analyzeDelivered ( m : any ) : { headline : string ; explanation : string ; severity : 'high' | 'medium' | 'low' ; actions : AnalysisAction [ ] } {
const score : number = m . spamScore ? ? 0 ;
const level : string = ( m . detectionLevel ? ? '' ) . toLowerCase ( ) ;
const status : string = ( m . status ? ? '' ) . toLowerCase ( ) ;
const from : string = m . from ? ? '' ;
2026-04-01 09:42:37 -04:00
const subject : string = m . subject ? ? '' ;
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
const fromDomain = from . includes ( '@' ) ? from . split ( '@' ) [ 1 ] : from ;
const fromEnvDomain = m . fromEnv ? . includes ( '@' ) ? m . fromEnv . split ( '@' ) [ 1 ] : '' ;
const envelopeMismatch = fromEnvDomain && fromDomain && fromEnvDomain !== fromDomain ;
2026-04-01 09:42:37 -04:00
// Subject-based threat detection — catches zero-score phishing/sextortion
const subjectThreat = detectSubjectThreat ( subject ) ;
if ( subjectThreat === 'sextortion' ) {
return {
headline : 'Sextortion Scam — Bypassed Spam Filter' ,
severity : 'high' ,
explanation : ` This is a known sextortion scam pattern. Despite a spam score of ${ score } , these emails evade spam filters because they use plain text (no links or attachments), send from free email providers like Gmail with good sender reputation ( ${ fromDomain } ), and send individually rather than in bulk — all of which make them invisible to volume-based spam detection. The sender has no actual recordings or access; this is a social engineering attempt to extort payment, typically in cryptocurrency. ` ,
actions : [
{ label : 'Block this sender immediately' , description : ` Add " ${ from } " to Mimecast > Administration > Gateway > Policies > Blocked Senders. Also add the domain " ${ fromDomain } " if it is not a legitimate provider. ` , type : 'info' , warning : true } ,
{ label : 'Enable Content Examination policy' , description : 'In Mimecast: Administration > Gateway > Policies > Content Examination. Create a rule to hold/reject messages containing keywords like "bitcoin", "I recorded you", "I hacked". This catches sextortion that spam scores miss.' , type : 'info' } ,
{ label : 'Report to Mimecast threat intel' , description : 'Forward the raw email as an attachment to abuse@mimecast.com to improve detection for all customers.' , type : 'info' } ,
{ label : 'Advise the recipient' , description : 'Let the recipient know this is a scam. They should not respond, not pay, and delete the email. No credentials were actually compromised.' , type : 'info' } ,
] ,
} ;
}
if ( subjectThreat === 'phishing' ) {
return {
headline : 'Suspected Phishing — Bypassed Spam Filter' ,
severity : 'high' ,
explanation : ` The subject line matches known phishing patterns. Despite a spam score of ${ score } , phishing emails frequently score 0 because they use legitimate sending infrastructure, contain no bulk-send signatures, and rely on social engineering rather than technical spam traits. The sender ( ${ from } ) should be verified before any action is taken on this email. ` ,
actions : [
{ label : 'Block this sender' , description : ` Add " ${ from } " to Mimecast > Administration > Gateway > Policies > Blocked Senders. ` , type : 'info' , warning : true } ,
{ label : 'Enable Impersonation Protection' , description : 'In Mimecast: Administration > Gateway > Policies > Impersonation Protection. Enable checks for display name spoofing and lookalike domains.' , type : 'info' } ,
{ label : 'Enable Content Examination' , description : 'Create a Mimecast Content Examination policy to hold messages matching phishing keyword patterns.' , type : 'info' } ,
{ label : 'Report to Mimecast' , description : 'Forward the raw email as an attachment to abuse@mimecast.com.' , type : 'info' } ,
] ,
} ;
}
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
if ( score >= 10 || level === 'high' ) {
return {
headline : 'High Spam Score — Delivered' ,
severity : 'high' ,
explanation : ` This message scored ${ score } on Mimecast's spam engine and was still delivered. A score of 10+ typically indicates bulk spam infrastructure or known spam signatures. The ${ envelopeMismatch ? ` envelope sender ( ${ m . fromEnv } ) differs from the header From ( ${ from } ), which is a common indicator of spoofing or mailing list abuse. ` : '' } message passed through without being held, likely because no policy threshold was set at this score level. ` ,
actions : [
{ label : 'Review spam policy thresholds' , description : 'In Mimecast: Administration > Gateway > Policies > Spam Scanning. Consider lowering the "hold" threshold to catch messages with scores ≥10.' , type : 'info' } ,
{ label : 'Block this sender domain' , description : ` Add " ${ fromDomain } " to Administration > Gateway > Policies > Blocked Senders to prevent future delivery from this domain. ` , type : 'info' } ,
{ label : 'Report as spam' , description : 'Forward the email as an attachment to abuse@mimecast.com to improve future detection.' , type : 'info' } ,
] ,
} ;
}
if ( score >= 5 || level === 'moderate' ) {
return {
headline : 'Moderate Spam Score — Delivered' ,
severity : 'medium' ,
explanation : ` This message scored ${ score } on spam detection (detection level: ${ m . detectionLevel || 'moderate' } ) but was delivered because it fell below the hold threshold. ${ envelopeMismatch ? ` The envelope sender ( ${ m . fromEnv } ) differs from the header From ( ${ from } ), suggesting use of a third-party sending platform. ` : '' } This may be legitimate marketing mail or a marginal false negative. ` ,
actions : [
{ label : 'Add to blocked senders' , description : ` If this is unwanted, add " ${ fromDomain } " to Mimecast > Administration > Gateway > Policies > Blocked Senders. ` , type : 'info' } ,
{ label : 'Adjust spam hold threshold' , description : 'Lower the spam hold threshold in Mimecast Spam Scanning policy to hold messages with scores ≥5 for admin review.' , type : 'info' } ,
] ,
} ;
}
if ( envelopeMismatch ) {
return {
headline : 'Envelope / Header Mismatch' ,
severity : 'medium' ,
explanation : ` The email's envelope sender ( ${ m . fromEnv } ) doesn't match the From header ( ${ from } ). This is common with third-party sending platforms (Mailchimp, SendGrid, HubSpot) but can also indicate spoofing. Spam score was ${ score } . The message was delivered. ` ,
actions : [
{ label : 'Verify the sender' , description : 'Check whether the sending platform is authorised to send on behalf of this domain (SPF/DKIM). Contact the sender via another channel if unsure.' , type : 'info' } ,
{ label : 'Add DMARC bypass if legitimate' , description : ` If this is a known sender using a third-party platform, add " ${ fromDomain } " to a Mimecast permitted sender policy. ` , type : 'info' } ,
] ,
} ;
}
if ( status === 'rejected' || status === 'bounced' ) {
return {
headline : ` Message ${ status === 'rejected' ? 'Rejected' : 'Bounced' } ` ,
severity : 'low' ,
explanation : ` This message was ${ status } — it was not delivered to the recipient. ${ status === 'rejected' ? 'Mimecast or the destination server rejected it during the SMTP session.' : 'It was accepted but subsequently bounced by the destination mailbox.' } ` ,
actions : [
{ label : 'Check recipient mailbox' , description : 'Verify the recipient address is valid and the mailbox is not full or disabled.' , type : 'info' } ,
] ,
} ;
}
return {
headline : 'Delivered — Clean' ,
severity : 'low' ,
explanation : ` This message was delivered with a spam score of ${ score } and no threat flags. Status: ${ m . status } . No action is required. ` ,
actions : [
{ label : 'No action needed' , description : 'This message appears clean. If you believe it is malicious, report it via the Mimecast console.' , type : 'info' } ,
] ,
} ;
}
2026-04-01 10:00:20 -04:00
function DeliveredAnalysisDialog ( { message , onClose , onFindSimilar , allMessages } : {
message : any ;
onClose : ( ) = > void ;
onFindSimilar ? : ( type : 'sender' | 'ip' | 'subject' , value : string ) = > void ;
allMessages? : any [ ] ;
} ) {
2026-04-01 10:09:38 -04:00
const [ remedStep , setRemedStep ] = useState < 'idle' | 'searching' | 'confirm' | 'removing' | 'done' > ( 'idle' ) ;
const [ remedMatches , setRemedMatches ] = useState < any [ ] > ( [ ] ) ;
const [ remedSelected , setRemedSelected ] = useState < Set < string > > ( new Set ( ) ) ;
const [ remedResults , setRemedResults ] = useState < { succeeded : number ; failed : number } | null > ( null ) ;
const [ remedError , setRemedError ] = useState < string | null > ( null ) ;
const [ permError , setPermError ] = useState < string | null > ( null ) ;
// Reset remediation state when message changes
const prevMessageId = message ? . id ;
useEffect ( ( ) = > {
setRemedStep ( 'idle' ) ;
setRemedMatches ( [ ] ) ;
setRemedSelected ( new Set ( ) ) ;
setRemedResults ( null ) ;
setRemedError ( null ) ;
setPermError ( null ) ;
} , [ prevMessageId ] ) ;
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
if ( ! message ) return null ;
const analysis = analyzeDelivered ( message ) ;
2026-04-01 10:09:38 -04:00
const searchMailbox = async ( ) = > {
setRemedStep ( 'searching' ) ;
setRemedError ( null ) ;
setPermError ( null ) ;
try {
const res = await fetch ( '/api/mimecast/mailbox-remediate' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { action : 'search' , userEmail : message.to , fromAddress : message.from } ) ,
} ) ;
if ( ! res . ok ) {
2026-04-05 08:55:41 -04:00
const isJson = res . headers . get ( 'content-type' ) ? . includes ( 'application/json' ) ;
const d = isJson ? await res . json ( ) : null ;
if ( d ? . permissionRequired ) { setPermError ( d . detail ) ; setRemedStep ( 'idle' ) ; return ; }
throw new Error ( d ? . error ? ? ` HTTP ${ res . status } ` ) ;
2026-04-01 10:09:38 -04:00
}
2026-04-05 08:55:41 -04:00
const d = await res . json ( ) ;
2026-04-01 10:09:38 -04:00
const matches = d . messages ? ? [ ] ;
setRemedMatches ( matches ) ;
setRemedSelected ( new Set ( matches . map ( ( m : any ) = > m . id ) ) ) ;
setRemedStep ( 'confirm' ) ;
} catch ( e : any ) {
setRemedError ( e . message ) ;
setRemedStep ( 'idle' ) ;
}
} ;
const removeSelected = async ( ) = > {
setRemedStep ( 'removing' ) ;
setRemedError ( null ) ;
try {
const res = await fetch ( '/api/mimecast/mailbox-remediate' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { action : 'move' , userEmail : message.to , messageIds : [ . . . remedSelected ] } ) ,
} ) ;
2026-04-05 08:55:41 -04:00
if ( ! res . ok ) {
const isJson = res . headers . get ( 'content-type' ) ? . includes ( 'application/json' ) ;
const d = isJson ? await res . json ( ) : null ;
throw new Error ( d ? . error ? ? ` HTTP ${ res . status } ` ) ;
}
2026-04-01 10:09:38 -04:00
const d = await res . json ( ) ;
setRemedResults ( { succeeded : d.succeeded , failed : d.failed } ) ;
setRemedStep ( 'done' ) ;
} catch ( e : any ) {
setRemedError ( e . message ) ;
setRemedStep ( 'confirm' ) ;
}
} ;
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
const severityBar = { high : 'border-l-red-500' , medium : 'border-l-amber-500' , low : 'border-l-blue-500' } ;
const severityBadge = {
high : 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-400' ,
medium : 'bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400' ,
low : 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400' ,
} ;
const severityLabel = { high : 'High risk' , medium : 'Review needed' , low : 'Clean' } ;
const severityIcon = {
high : < XCircle className = "w-4 h-4" / > ,
medium : < AlertTriangle className = "w-4 h-4" / > ,
low : < CheckCircle2 className = "w-4 h-4" / > ,
} ;
return (
< Dialog open = { ! ! message } onOpenChange = { open = > ! open && onClose ( ) } >
< DialogContent className = "max-w-xl w-full max-h-[85vh] overflow-y-auto" >
< DialogHeader >
< div className = "flex items-center gap-2 flex-wrap" >
< DialogTitle className = "text-base" > { analysis . headline } < / DialogTitle >
< span className = { ` inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${ severityBadge [ analysis . severity ] } ` } >
{ severityIcon [ analysis . severity ] }
{ severityLabel [ analysis . severity ] }
< / span >
< / div >
< / DialogHeader >
{ /* Message details */ }
< div className = "rounded-md border bg-muted/20 divide-y text-sm" >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Subject < / span >
< span className = "font-medium break-words" > { message . subject || '(no subject)' } < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > From < / span >
< div >
< div className = "break-all" > { message . from } < / div >
{ message . fromEnv && message . fromEnv !== message . from && (
< div className = "text-xs text-muted-foreground mt-0.5" > Envelope : { message . fromEnv } < / div >
) }
< / div >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > To < / span >
< span > { message . toDisplay ? ` ${ message . toDisplay } < ${ message . to } > ` : message . to } < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Received < / span >
< span > { new Date ( message . received ) . toLocaleString ( ) } < / span >
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Status < / span >
< span className = { ` inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium w-fit ${
message . status === 'accepted' ? 'bg-green-500/10 text-green-700'
: message . status === 'held' ? 'bg-amber-500/10 text-amber-700'
: message . status === 'rejected' || message . status === 'bounced' ? 'bg-red-500/10 text-red-600'
: 'bg-muted text-muted-foreground'
} ` }>{message.status}</span>
< / div >
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Spam score < / span >
< div className = "flex items-center gap-2" >
< span className = { ` font-medium ${ message . spamScore >= 10 ? 'text-red-600' : message . spamScore >= 5 ? 'text-amber-600' : 'text-green-700' } ` } >
{ message . spamScore }
< / span >
{ message . detectionLevel && (
< span className = "text-xs text-muted-foreground" > ( { message . detectionLevel } ) < / span >
) }
< div className = "flex-1 h-1.5 rounded-full bg-muted overflow-hidden max-w-24" >
< div className = { ` h-full rounded-full ${ message . spamScore >= 10 ? 'bg-red-500' : message . spamScore >= 5 ? 'bg-amber-500' : 'bg-green-500' } ` }
style = { { width : ` ${ Math . min ( 100 , ( message . spamScore / 20 ) * 100 ) } % ` } } / >
< / div >
< / div >
< / div >
{ message . senderIP && (
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Sender IP < / span >
< span className = "font-mono text-xs" > { message . senderIP } < / span >
< / div >
) }
< div className = "grid grid-cols-[72px_1fr] gap-2 px-3 py-2" >
< span className = "text-muted-foreground text-xs pt-0.5" > Attachments < / span >
< span className = "text-muted-foreground" > { message . attachments ? 'Yes' : 'No' } < / span >
< / div >
< / div >
{ /* Explanation */ }
< div className = { ` rounded-md border-l-4 border border-border pl-3 pr-3 py-2.5 text-sm text-foreground/90 ${ severityBar [ analysis . severity ] } ` } >
{ analysis . explanation }
< / div >
{ /* Actions */ }
< div className = "space-y-1.5" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > Recommended actions < / p >
< div className = "space-y-2" >
{ analysis . actions . map ( ( action , i ) = > (
< div key = { i } className = "rounded-md border p-3 space-y-0.5" >
< p className = "text-sm font-medium" > { action . label } < / p >
< p className = "text-xs text-muted-foreground" > { action . description } < / p >
< / div >
) ) }
< / div >
< / div >
2026-04-01 10:00:20 -04:00
{ /* Cluster context — show how many other messages match sender/IP in current results */ }
{ onFindSimilar && allMessages && allMessages . length > 1 && ( ( ) = > {
const sameFrom = allMessages . filter ( x = > x . from === message . from && x . id !== message . id ) ;
const sameIP = message . senderIP ? allMessages . filter ( x = > x . senderIP === message . senderIP && x . id !== message . id ) : [ ] ;
const subjectWords = ( message . subject ? ? '' ) . split ( /\s+/ ) . slice ( 0 , 5 ) . join ( ' ' ) ;
const sameSubject = subjectWords . length > 8 ? allMessages . filter ( x = > x . id !== message . id && ( x . subject ? ? '' ) . startsWith ( subjectWords ) ) : [ ] ;
const hasClusters = sameFrom . length > 0 || sameIP . length > 0 || sameSubject . length > 0 ;
if ( ! hasClusters ) return null ;
return (
< div className = "rounded-md border border-amber-300/60 bg-amber-50/40 dark:bg-amber-950/10 p-3 space-y-2" >
< p className = "text-xs font-semibold text-amber-700 dark:text-amber-400 uppercase tracking-wide" > Pattern matches in current results < / p >
< div className = "space-y-1.5" >
{ sameFrom . length > 0 && (
< div className = "flex items-center justify-between gap-2" >
< span className = "text-xs text-foreground/80" >
< span className = "font-medium" > { sameFrom . length + 1 } messages < / span > from < span className = "font-mono" > { message . from } < / span >
{ ' ' } → { [ . . . new Set ( [ message . to , . . . sameFrom . map ( ( x : any ) = > x . to ) ] ) ] . join ( ', ' ) }
< / span >
< Button size = "sm" variant = "outline" className = "h-6 text-xs px-2 flex-shrink-0"
onClick = { ( ) = > { onClose ( ) ; onFindSimilar ( 'sender' , message . from ) ; } } >
Find all
< / Button >
< / div >
) }
{ sameIP . length > 0 && sameIP . length !== sameFrom . length && (
< div className = "flex items-center justify-between gap-2" >
< span className = "text-xs text-foreground/80" >
< span className = "font-medium" > { sameIP . length + 1 } messages < / span > from IP < span className = "font-mono" > { message . senderIP } < / span >
{ ' ' } ( multiple senders )
< / span >
< Button size = "sm" variant = "outline" className = "h-6 text-xs px-2 flex-shrink-0"
onClick = { ( ) = > { onClose ( ) ; onFindSimilar ( 'ip' , message . senderIP ) ; } } >
Find all
< / Button >
< / div >
) }
{ sameSubject . length > 0 && (
< div className = "flex items-center justify-between gap-2" >
< span className = "text-xs text-foreground/80" >
< span className = "font-medium" > { sameSubject . length + 1 } messages < / span > with similar subject
< / span >
< Button size = "sm" variant = "outline" className = "h-6 text-xs px-2 flex-shrink-0"
onClick = { ( ) = > { onClose ( ) ; onFindSimilar ( 'subject' , subjectWords ) ; } } >
Find all
< / Button >
< / div >
) }
< / div >
< / div >
) ;
} ) ( ) }
2026-04-01 10:09:38 -04:00
{ /* Mailbox Remediation */ }
< div className = "rounded-md border overflow-hidden" >
< div className = "flex items-center justify-between px-3 py-2.5 bg-muted/20" >
< div className = "flex items-center gap-2" >
< Trash2 className = "w-3.5 h-3.5 text-muted-foreground" / >
< span className = "text-sm font-medium" > Remove from mailbox < / span >
< span className = "text-xs text-muted-foreground" > — search { message . to } ' s mailbox and delete < / span >
< / div >
{ remedStep === 'idle' && (
< Button size = "sm" variant = "outline" className = "h-7 text-xs px-2 text-red-700 border-red-300 hover:bg-red-50 dark:hover:bg-red-950/20"
onClick = { searchMailbox } >
< Search className = "w-3 h-3 mr-1" / > Search mailbox
< / Button >
) }
< / div >
{ permError && (
< div className = "px-3 py-2.5 space-y-1.5 border-t" >
< p className = "text-xs font-semibold text-amber-700 dark:text-amber-400" > Permission required < / p >
< p className = "text-xs text-muted-foreground" > { permError } < / p >
< p className = "text-xs font-mono bg-muted/40 rounded px-2 py-1" > Mail . ReadWrite ( Application ) < / p >
< / div >
) }
{ remedError && (
< div className = "px-3 py-2 border-t text-xs text-red-600" > { remedError } < / div >
) }
{ remedStep === 'searching' && (
< div className = "px-3 py-3 border-t flex items-center gap-2 text-sm text-muted-foreground" >
< Loader2 className = "w-4 h-4 animate-spin" / > Searching { message . to } ' s mailbox …
< / div >
) }
{ remedStep === 'confirm' && (
< div className = "border-t divide-y" >
{ remedMatches . length === 0 ? (
< div className = "px-3 py-2.5 text-xs text-muted-foreground" > No matching messages found in mailbox . < / div >
) : (
< >
< div className = "px-3 py-2 text-xs text-muted-foreground bg-muted/10" >
Found < span className = "font-semibold text-foreground" > { remedMatches . length } < / span > message { remedMatches . length !== 1 ? 's' : '' } in mailbox — select to move to Deleted Items :
< / div >
< div className = "max-h-40 overflow-y-auto divide-y" >
{ remedMatches . map ( m = > (
< label key = { m . id } className = "flex items-start gap-2 px-3 py-1.5 hover:bg-muted/20 cursor-pointer" >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Checkbox
className = "mt-0.5 flex-shrink-0"
2026-04-01 10:09:38 -04:00
checked = { remedSelected . has ( m . id ) }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
onCheckedChange = { ( v ) = > {
2026-04-01 10:09:38 -04:00
const s = new Set ( remedSelected ) ;
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
if ( v === true ) s . add ( m . id ) ; else s . delete ( m . id ) ;
2026-04-01 10:09:38 -04:00
setRemedSelected ( s ) ;
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
} }
/ >
2026-04-01 10:09:38 -04:00
< div className = "min-w-0" >
< div className = "text-xs truncate" > { m . subject || '(no subject)' } < / div >
< div className = "text-xs text-muted-foreground" >
{ new Date ( m . receivedDateTime ) . toLocaleString ( undefined , { month : 'short' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) }
{ ! m . isRead && < span className = "ml-1 text-blue-500 font-medium" > unread < / span > }
< / div >
< / div >
< / label >
) ) }
< / div >
< div className = "px-3 py-2 flex items-center justify-between gap-2" >
< span className = "text-xs text-muted-foreground" > { remedSelected . size } selected — will move to Deleted Items ( recoverable ) < / span >
< div className = "flex gap-2" >
< Button size = "sm" variant = "ghost" className = "h-7 text-xs px-2"
onClick = { ( ) = > setRemedStep ( 'idle' ) } > Cancel < / Button >
< Button size = "sm" variant = "destructive" className = "h-7 text-xs px-2"
disabled = { remedSelected . size === 0 }
onClick = { removeSelected } >
< Trash2 className = "w-3 h-3 mr-1" / >
Move { remedSelected . size } to Deleted
< / Button >
< / div >
< / div >
< / >
) }
< / div >
) }
{ remedStep === 'removing' && (
< div className = "px-3 py-3 border-t flex items-center gap-2 text-sm text-muted-foreground" >
< Loader2 className = "w-4 h-4 animate-spin" / > Removing { remedSelected . size } message { remedSelected . size !== 1 ? 's' : '' } …
< / div >
) }
{ remedStep === 'done' && remedResults && (
< div className = "px-3 py-2.5 border-t flex items-center gap-2" >
{ remedResults . failed === 0
? < CheckCircle2 className = "w-4 h-4 text-green-500 flex-shrink-0" / >
: < AlertTriangle className = "w-4 h-4 text-amber-500 flex-shrink-0" / > }
< span className = "text-xs" >
< span className = "font-medium" > { remedResults . succeeded } < / span > message { remedResults . succeeded !== 1 ? 's' : '' } moved to Deleted Items
{ remedResults . failed > 0 && ` , ${ remedResults . failed } failed ` }
< / span >
< / div >
) }
< / div >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< div className = "rounded-md border border-dashed px-3 py-2.5 text-xs text-muted-foreground flex items-start gap-2" >
< ExternalLink className = "w-3.5 h-3.5 mt-0.5 flex-shrink-0" / >
< span > View full message tracking in the Mimecast Administration Console under Gateway & gt ; Message Center & gt ; Message Finder .
< a href = "https://admin.services.mimecast.com" target = "_blank" rel = "noopener noreferrer" className = "ml-1 underline hover:text-foreground" > Open Console → < / a >
< / span >
< / div >
< / DialogContent >
< / Dialog >
) ;
}
function DeliveredMailTab() {
const [ tenantId , setTenantId ] = useState ( '1' ) ;
const [ to , setTo ] = useState ( '' ) ;
const [ from , setFrom ] = useState ( '' ) ;
const [ subject , setSubject ] = useState ( '' ) ;
const [ startHours , setStartHours ] = useState ( 24 ) ;
const [ messages , setMessages ] = useState < any [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
const [ loaded , setLoaded ] = useState ( false ) ;
const [ statusFilter , setStatusFilter ] = useState ( '' ) ;
const [ sortBy , setSortBy ] = useState < 'received' | 'spamScore' > ( 'received' ) ;
const [ analysisMessage , setAnalysisMessage ] = useState < any > ( null ) ;
2026-04-01 10:00:20 -04:00
const [ clusterExpanded , setClusterExpanded ] = useState ( true ) ;
const handleFindSimilar = ( type : 'sender' | 'ip' | 'subject' , value : string ) = > {
if ( type === 'sender' ) {
const domain = value . includes ( '@' ) ? value . split ( '@' ) [ 1 ] : value ;
const isFreemail = [ 'gmail.com' , 'yahoo.com' , 'hotmail.com' , 'outlook.com' , 'live.com' , 'aol.com' , 'icloud.com' ] . includes ( domain ) ;
if ( isFreemail ) {
setFrom ( value ) ;
} else {
setFrom ( '@' + domain ) ;
}
} else if ( type === 'ip' ) {
setFrom ( '' ) ;
setSubject ( '' ) ;
} else if ( type === 'subject' ) {
setSubject ( value ) ;
setFrom ( '' ) ;
}
setTimeout ( ( ) = > searchWithOverrides ( type , value ) , 50 ) ;
} ;
const searchWithOverrides = async ( type : 'sender' | 'ip' | 'subject' , value : string ) = > {
setLoading ( true ) ;
setLoadError ( null ) ;
setLoaded ( false ) ;
try {
let body : any = { tenantId , startHours } ;
if ( type === 'sender' ) {
const domain = value . includes ( '@' ) ? value . split ( '@' ) [ 1 ] : value ;
const isFreemail = [ 'gmail.com' , 'yahoo.com' , 'hotmail.com' , 'outlook.com' , 'live.com' , 'aol.com' , 'icloud.com' ] . includes ( domain ) ;
body . from = isFreemail ? value : '@' + domain ;
} else if ( type === 'subject' ) {
body . subject = value ;
}
if ( ! body . from && ! body . subject && ! to ) body . to = to || undefined ;
if ( ! body . from && ! body . to && ! body . subject ) { setLoading ( false ) ; return ; }
const res = await fetch ( '/api/mimecast/delivered' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( body ) ,
} ) ;
if ( ! res . ok ) { const t = await res . text ( ) ; throw new Error ( ` HTTP ${ res . status } : ${ t . slice ( 0 , 200 ) } ` ) ; }
const d = await res . json ( ) ;
setMessages ( d . messages ? ? [ ] ) ;
setLoaded ( true ) ;
} catch ( e : any ) {
setLoadError ( e . message ? ? 'Unknown error' ) ;
} finally {
setLoading ( false ) ;
}
} ;
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
const search = async ( ) = > {
if ( ! to && ! from && ! subject ) return ;
setLoading ( true ) ;
setLoadError ( null ) ;
try {
const res = await fetch ( '/api/mimecast/delivered' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { tenantId , to : to || undefined , from : from || undefined , subject : subject || undefined , startHours } ) ,
} ) ;
if ( ! res . ok ) {
const t = await res . text ( ) ;
throw new Error ( ` HTTP ${ res . status } : ${ t . slice ( 0 , 200 ) } ` ) ;
}
const d = await res . json ( ) ;
setMessages ( d . messages ? ? [ ] ) ;
setLoaded ( true ) ;
} catch ( e : any ) {
setLoadError ( e . message ? ? 'Unknown error' ) ;
} finally {
setLoading ( false ) ;
}
} ;
const filtered = messages
. filter ( m = > ! statusFilter || m . status === statusFilter )
. sort ( ( a , b ) = > sortBy === 'spamScore'
? ( b . spamScore ? ? 0 ) - ( a . spamScore ? ? 0 )
: new Date ( b . received ) . getTime ( ) - new Date ( a . received ) . getTime ( )
) ;
const statuses = [ . . . new Set ( messages . map ( m = > m . status ) . filter ( Boolean ) ) ] . sort ( ) ;
2026-04-01 09:46:16 -04:00
const highRisk = messages . filter ( m = > ( m . spamScore ? ? 0 ) >= 10 || detectSubjectThreat ( m . subject ) !== null ) . length ;
const medRisk = messages . filter ( m = > ( m . spamScore ? ? 0 ) >= 5 && ( m . spamScore ? ? 0 ) < 10 && detectSubjectThreat ( m . subject ) === null ) . length ;
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
return (
< div className = "space-y-4" >
{ /* Search controls */ }
< div className = "rounded-lg border p-4 space-y-3" >
< div className = "flex items-center gap-2 mb-1" >
< TrendingUp className = "w-4 h-4 text-muted-foreground" / >
< span className = "text-sm font-medium" > Search Delivered Mail < / span >
< span className = "text-xs text-muted-foreground ml-1" > — find messages that passed through Mimecast < / span >
< / div >
2026-04-01 09:29:52 -04:00
< div className = "flex flex-wrap gap-3 items-end" >
< div className = "w-40 flex-shrink-0" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" > Tenant < / label >
< select value = { tenantId } onChange = { e = > { setTenantId ( e . target . value ) ; setLoaded ( false ) ; setMessages ( [ ] ) ; } }
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background" >
{ TENANT_OPTIONS . map ( t = > < option key = { t . id } value = { t . id } > { t . name } < / option > ) }
< / select >
< / div >
2026-04-01 09:29:52 -04:00
< div className = "flex-1 min-w-44" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" > Recipient ( to ) < / label >
< input type = "text" placeholder = "user@domain.com" value = { to }
onChange = { e = > setTo ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && search ( ) }
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background" / >
< / div >
2026-04-01 09:29:52 -04:00
< div className = "flex-1 min-w-44" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" > Sender ( from ) < / label >
< input type = "text" placeholder = "sender@domain.com" value = { from }
onChange = { e = > setFrom ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && search ( ) }
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background" / >
< / div >
2026-04-01 09:29:52 -04:00
< div className = "flex-1 min-w-44" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" > Subject contains < / label >
< input type = "text" placeholder = "keyword…" value = { subject }
onChange = { e = > setSubject ( e . target . value ) }
onKeyDown = { e = > e . key === 'Enter' && search ( ) }
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background" / >
< / div >
2026-04-01 09:29:52 -04:00
< div className = "w-36 flex-shrink-0" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< label className = "text-xs text-muted-foreground mb-1 block" > Time range < / label >
< select value = { startHours } onChange = { e = > setStartHours ( Number ( e . target . value ) ) }
2026-04-01 09:29:52 -04:00
className = "w-full border rounded-md px-3 py-1.5 text-sm bg-background" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< option value = { 6 } > Last 6 hours < / option >
< option value = { 24 } > Last 24 hours < / option >
< option value = { 48 } > Last 48 hours < / option >
< option value = { 72 } > Last 72 hours < / option >
< option value = { 168 } > Last 7 days < / option >
< / select >
< / div >
2026-04-01 09:29:52 -04:00
< Button onClick = { search } disabled = { loading || ( ! to && ! from && ! subject ) } className = "gap-2 flex-shrink-0" >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
{ loading ? < Loader2 className = "w-4 h-4 animate-spin" / > : < Search className = "w-4 h-4" / > }
Search
< / Button >
< / div >
2026-04-01 09:29:52 -04:00
{ ! to && ! from && ! subject && (
< p className = "text-xs text-muted-foreground" > Enter recipient , sender , or subject to search < / p >
) }
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< / div >
{ loadError && (
< div className = "rounded-lg border border-red-300 bg-red-50 dark:bg-red-950/20 p-3 text-sm text-red-600" > { loadError } < / div >
) }
{ loading && (
< div className = "flex items-center justify-center py-16" >
< Loader2 className = "w-6 h-6 animate-spin text-muted-foreground" / >
< span className = "ml-2 text-sm text-muted-foreground" > Searching message logs … < / span >
< / div >
) }
{ /* Summary stats */ }
{ loaded && ! loading && messages . length > 0 && (
< div className = "flex flex-wrap gap-3" >
< div className = "rounded-lg border px-4 py-2 text-sm" >
< span className = "text-muted-foreground" > Total < / span >
< span className = "font-semibold" > { messages . length } < / span >
< / div >
{ highRisk > 0 && (
< div className = "rounded-lg border border-red-300 bg-red-50/50 dark:bg-red-950/10 px-4 py-2 text-sm text-red-700 dark:text-red-400" >
< XCircle className = "w-3.5 h-3.5 inline mr-1" / >
< span className = "font-semibold" > { highRisk } < / span > high spam score ( ≥ 10 )
< / div >
) }
{ medRisk > 0 && (
< div className = "rounded-lg border border-amber-300 bg-amber-50/50 dark:bg-amber-950/10 px-4 py-2 text-sm text-amber-700 dark:text-amber-400" >
< AlertTriangle className = "w-3.5 h-3.5 inline mr-1" / >
< span className = "font-semibold" > { medRisk } < / span > moderate spam score ( 5 – 9 )
< / div >
) }
< div className = "rounded-lg border border-green-300 bg-green-50/50 dark:bg-green-950/10 px-4 py-2 text-sm text-green-700 dark:text-green-400" >
< CheckCircle2 className = "w-3.5 h-3.5 inline mr-1" / >
< span className = "font-semibold" > { messages . length - highRisk - medRisk } < / span > clean
< / div >
< / div >
) }
2026-04-01 10:00:20 -04:00
{ /* Cluster / Pattern Analysis */ }
{ loaded && ! loading && messages . length > 1 && ( ( ) = > {
// Group by sender email
const bySender : Record < string , any [ ] > = { } ;
for ( const m of messages ) { if ( m . from ) { ( bySender [ m . from ] ? ? = [ ] ) . push ( m ) ; } }
const topSenders = Object . entries ( bySender ) . filter ( ( [ , v ] ) = > v . length > 1 )
. sort ( ( a , b ) = > b [ 1 ] . length - a [ 1 ] . length ) . slice ( 0 , 5 ) ;
// Group by sender IP
const byIP : Record < string , any [ ] > = { } ;
for ( const m of messages ) { if ( m . senderIP ) { ( byIP [ m . senderIP ] ? ? = [ ] ) . push ( m ) ; } }
const topIPs = Object . entries ( byIP ) . filter ( ( [ , v ] ) = > v . length > 1 )
. sort ( ( a , b ) = > b [ 1 ] . length - a [ 1 ] . length ) . slice ( 0 , 5 ) ;
// Group by subject prefix (first 5 words)
const bySubject : Record < string , any [ ] > = { } ;
for ( const m of messages ) {
const prefix = ( m . subject ? ? '' ) . split ( /\s+/ ) . slice ( 0 , 5 ) . join ( ' ' ) . toLowerCase ( ) ;
if ( prefix . length > 5 ) { ( bySubject [ prefix ] ? ? = [ ] ) . push ( m ) ; }
}
const topSubjects = Object . entries ( bySubject ) . filter ( ( [ , v ] ) = > v . length > 1 )
. sort ( ( a , b ) = > b [ 1 ] . length - a [ 1 ] . length ) . slice ( 0 , 3 ) ;
if ( ! topSenders . length && ! topIPs . length && ! topSubjects . length ) return null ;
return (
< div className = "rounded-lg border overflow-hidden" >
< button
className = "w-full flex items-center justify-between px-4 py-2.5 bg-muted/30 hover:bg-muted/50 text-left"
onClick = { ( ) = > setClusterExpanded ( v = > ! v ) }
>
< div className = "flex items-center gap-2" >
< Search className = "w-3.5 h-3.5 text-muted-foreground" / >
< span className = "text-sm font-medium" > Pattern Analysis < / span >
< span className = "text-xs text-muted-foreground" > — repeated senders , IPs , subjects in these results < / span >
< / div >
{ clusterExpanded ? < ChevronDown className = "w-4 h-4 text-muted-foreground" / > : < ChevronRight className = "w-4 h-4 text-muted-foreground" / > }
< / button >
{ clusterExpanded && (
< div className = "divide-y" >
{ topSenders . length > 0 && (
< div className = "px-4 py-3 space-y-2" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > Repeated senders < / p >
< div className = "space-y-1.5" >
{ topSenders . map ( ( [ sender , msgs ] ) = > {
const recipients = [ . . . new Set ( msgs . map ( ( m : any ) = > m . to ) ) ] ;
const isThreat = msgs . some ( ( m : any ) = > detectSubjectThreat ( m . subject ) !== null || ( m . spamScore ? ? 0 ) >= 5 ) ;
return (
< div key = { sender } className = "flex items-center justify-between gap-3 text-sm" >
< div className = "min-w-0 flex items-start gap-2" >
{ isThreat && < XCircle className = "w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" / > }
< div className = "min-w-0" >
< span className = "font-mono text-xs break-all" > { sender } < / span >
< div className = "text-xs text-muted-foreground" >
{ msgs . length } messages → { recipients . length } recipient { recipients . length !== 1 ? 's' : '' } : { recipients . slice ( 0 , 3 ) . join ( ', ' ) } { recipients . length > 3 ? ` + ${ recipients . length - 3 } more ` : '' }
< / div >
< / div >
< / div >
< Button size = "sm" variant = { isThreat ? 'destructive' : 'outline' } className = "h-6 text-xs px-2 flex-shrink-0 opacity-80"
onClick = { ( ) = > handleFindSimilar ( 'sender' , sender ) } >
Search
< / Button >
< / div >
) ;
} ) }
< / div >
< / div >
) }
{ topIPs . length > 0 && (
< div className = "px-4 py-3 space-y-2" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > Repeated sending IPs < / p >
< div className = "space-y-1.5" >
{ topIPs . map ( ( [ ip , msgs ] ) = > {
const senders = [ . . . new Set ( msgs . map ( ( m : any ) = > m . from ) ) ] ;
const recipients = [ . . . new Set ( msgs . map ( ( m : any ) = > m . to ) ) ] ;
const isThreat = msgs . some ( ( m : any ) = > detectSubjectThreat ( m . subject ) !== null || ( m . spamScore ? ? 0 ) >= 5 ) ;
return (
< div key = { ip } className = "flex items-center justify-between gap-3 text-sm" >
< div className = "min-w-0 flex items-start gap-2" >
{ isThreat && < XCircle className = "w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" / > }
< div className = "min-w-0" >
< span className = "font-mono text-xs" > { ip } < / span >
< div className = "text-xs text-muted-foreground" >
{ msgs . length } messages · { senders . length } sender { senders . length !== 1 ? 's' : '' } · { recipients . length } recipient { recipients . length !== 1 ? 's' : '' }
{ senders . length <= 2 ? ` : ${ senders . join ( ', ' ) } ` : '' }
< / div >
< / div >
< / div >
< span className = "text-xs text-muted-foreground flex-shrink-0" > IP pivot N / A < / span >
< / div >
) ;
} ) }
< / div >
< / div >
) }
{ topSubjects . length > 0 && (
< div className = "px-4 py-3 space-y-2" >
< p className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" > Repeated subject patterns < / p >
< div className = "space-y-1.5" >
{ topSubjects . map ( ( [ prefix , msgs ] ) = > {
const senders = [ . . . new Set ( msgs . map ( ( m : any ) = > m . from ) ) ] ;
const recipients = [ . . . new Set ( msgs . map ( ( m : any ) = > m . to ) ) ] ;
const isThreat = msgs . some ( ( m : any ) = > detectSubjectThreat ( m . subject ) !== null ) ;
return (
< div key = { prefix } className = "flex items-center justify-between gap-3 text-sm" >
< div className = "min-w-0 flex items-start gap-2" >
{ isThreat && < XCircle className = "w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" / > }
< div className = "min-w-0" >
< span className = "text-xs truncate block" > "{msgs[0].subject}" < / span >
< div className = "text-xs text-muted-foreground" >
{ msgs . length } messages · { senders . length } sender { senders . length !== 1 ? 's' : '' } · { recipients . length } recipient { recipients . length !== 1 ? 's' : '' }
< / div >
< / div >
< / div >
< Button size = "sm" variant = { isThreat ? 'destructive' : 'outline' } className = "h-6 text-xs px-2 flex-shrink-0 opacity-80"
onClick = { ( ) = > handleFindSimilar ( 'subject' , prefix ) } >
Search
< / Button >
< / div >
) ;
} ) }
< / div >
< / div >
) }
< / div >
) }
< / div >
) ;
} ) ( ) }
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
{ /* Filters + sort */ }
{ loaded && ! loading && messages . length > 0 && (
< div className = "flex flex-wrap gap-2 items-center" >
{ statuses . length > 1 && (
< select value = { statusFilter } onChange = { e = > setStatusFilter ( e . target . value ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background" >
< option value = "" > All statuses < / option >
{ statuses . map ( s = > < option key = { s } value = { s } > { s } < / option > ) }
< / select >
) }
< select value = { sortBy } onChange = { e = > setSortBy ( e . target . value as any ) }
className = "border rounded-md px-3 py-1.5 text-sm bg-background" >
< option value = "received" > Sort by date < / option >
< option value = "spamScore" > Sort by spam score < / option >
< / select >
{ filtered . length !== messages . length && (
< span className = "text-xs text-muted-foreground" > { filtered . length } of { messages . length } shown < / span >
) }
< / div >
) }
{ loaded && ! loading && messages . length === 0 && (
< div className = "rounded-lg border p-12 text-center text-muted-foreground text-sm" >
No messages found for this search . Try a broader time range or different search terms .
< / div >
) }
{ loaded && ! loading && filtered . length > 0 && (
< div className = "rounded-lg border overflow-hidden" >
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< Table className = "table-fixed" >
< TableHeader className = "bg-muted/30" >
< TableRow >
< TableHead style = { { width : '110px' } } className = "text-xs" > Date < / TableHead >
< TableHead style = { { width : '150px' } } className = "text-xs" > To < / TableHead >
< TableHead style = { { width : '170px' } } className = "text-xs" > From < / TableHead >
< TableHead className = "text-xs" > Subject < / TableHead >
< TableHead style = { { width : '90px' } } className = "text-xs" > Status < / TableHead >
< TableHead style = { { width : '80px' } } className = "text-xs" > Spam < / TableHead >
< TableHead style = { { width : '90px' } } > < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
{ filtered . map ( ( m : any ) = > (
< TableRow key = { m . id } className = {
m . spamScore >= 10 || detectSubjectThreat ( m . subject ) !== null
? 'bg-red-500/5'
: m . spamScore >= 5
? 'bg-amber-500/5'
: ''
} >
< TableCell className = "text-muted-foreground whitespace-nowrap text-xs num" >
{ new Date ( m . received ) . toLocaleString ( undefined , { month : 'short' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) }
< / TableCell >
< TableCell className = "text-xs" style = { { overflow : 'hidden' } } >
< div className = "truncate" > { m . to } < / div >
< / TableCell >
< TableCell style = { { overflow : 'hidden' } } >
< div className = "text-xs truncate font-medium" > { m . from } < / div >
{ m . fromEnv && m . fromEnv !== m . from && (
< div className = "text-xs text-muted-foreground truncate" > { m . fromEnv } < / div >
) }
< / TableCell >
< TableCell className = "text-xs" style = { { overflow : 'hidden' } } >
< div className = "truncate" > { m . subject || '(no subject)' } < / div >
< / TableCell >
< TableCell >
< StatusBadge
tone = {
m . status === 'accepted' ? 'ok' :
m . status === 'held' ? 'warn' :
m . status === 'rejected' || m . status === 'bounced' ? 'error' :
'inactive'
}
>
{ m . status }
< / StatusBadge >
< / TableCell >
< TableCell >
< span className = { ` text-xs font-semibold num ${ m . spamScore >= 10 ? 'text-red-600' : m . spamScore >= 5 ? 'text-amber-600' : 'text-muted-foreground' } ` } >
{ m . spamScore }
< / span >
< / TableCell >
< TableCell >
< Button size = "sm" variant = "ghost" className = "h-7 text-xs px-2 whitespace-nowrap"
onClick = { ( ) = > setAnalysisMessage ( m ) } >
< Eye className = "w-3 h-3 mr-1" / >
View
< / Button >
< / TableCell >
< / TableRow >
) ) }
< / TableBody >
< / Table >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< / div >
) }
2026-04-01 10:00:20 -04:00
< DeliveredAnalysisDialog
message = { analysisMessage }
onClose = { ( ) = > setAnalysisMessage ( null ) }
onFindSimilar = { handleFindSimilar }
allMessages = { messages }
/ >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< / div >
) ;
}
2026-03-17 16:23:47 -04:00
// ── Page ──────────────────────────────────────────────────────────────────────
export default function MimecastSyncPage() {
const [ statusData , setStatusData ] = useState < any > ( null ) ;
const [ syncing , setSyncing ] = useState ( false ) ;
const [ lastResult , setLastResult ] = useState < any > ( null ) ;
const fetchStatus = ( ) = > {
fetch ( '/api/mimecast/status' )
. then ( r = > r . json ( ) )
. then ( d = > setStatusData ( d ) )
. catch ( ( ) = > setStatusData ( { configured : false , connected : false } ) ) ;
} ;
useEffect ( ( ) = > { fetchStatus ( ) ; } , [ ] ) ;
const handleSync = async ( syncType : string ) = > {
setSyncing ( true ) ;
setLastResult ( null ) ;
try {
const res = await fetch ( '/api/sync/mimecast' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { syncType } ) ,
} ) ;
const data = await res . json ( ) ;
setLastResult ( data ) ;
fetchStatus ( ) ;
} catch ( err : any ) {
setLastResult ( { error : err.message } ) ;
} finally {
setSyncing ( false ) ;
}
} ;
return (
< div className = "container mx-auto py-4 md:py-8 px-4 space-y-6" >
{ /* Header */ }
< div className = "flex items-center gap-4" >
< Link href = "/admin/sync" >
< Button variant = "outline" size = "sm" className = "gap-2" >
< ArrowLeft className = "w-4 h-4" / > Integrations
< / Button >
< / Link >
< div className = "flex items-center gap-3" >
< div className = "p-2 rounded-lg border border-blue-500/30 bg-blue-500/5" >
< Mail className = "w-5 h-5 text-blue-500" / >
< / div >
< div >
< h1 className = "text-2xl font-bold" > Email Security — Mimecast < / h1 >
< p className = "text-sm text-muted-foreground" > Message logs , threat events , 120 - day retention < / p >
< / div >
< / div >
< / div >
{ /* Last sync result banner */ }
{ lastResult && (
< div className = { ` rounded-lg border p-3 text-sm ${ lastResult . error ? 'border-red-300 bg-red-50 dark:bg-red-950/20 text-red-600' : 'border-green-300 bg-green-50 dark:bg-green-950/20 text-green-700' } ` } >
{ lastResult . error
? ` Sync failed: ${ lastResult . error } `
: ` Sync complete — ${ fmtNum ( lastResult . messagesUpserted ) } messages, ${ fmtNum ( lastResult . threatsUpserted ) } threats, ${ fmtNum ( lastResult . bodiesFetched ) } bodies fetched in ${ Math . round ( ( lastResult . durationMs ? ? 0 ) / 1000 ) } s `
}
{ lastResult . errors ? . length > 0 && (
< div className = "mt-1 text-xs opacity-80" > { lastResult . errors . slice ( 0 , 3 ) . join ( ' · ' ) } < / div >
) }
< / div >
) }
< Tabs defaultValue = "status" className = "w-full" >
2026-04-01 09:29:52 -04:00
< TabsList className = "flex w-full flex-wrap h-auto gap-0" >
< TabsTrigger value = "status" className = "gap-1.5 text-xs" > < Activity className = "h-3.5 w-3.5" / > Status < / TabsTrigger >
< TabsTrigger value = "held" className = "gap-1.5 text-xs" > < PauseCircle className = "h-3.5 w-3.5" / > Held Mail < / TabsTrigger >
< TabsTrigger value = "delivered" className = "gap-1.5 text-xs" > < TrendingUp className = "h-3.5 w-3.5" / > Delivered < / TabsTrigger >
< TabsTrigger value = "messages" className = "gap-1.5 text-xs" > < Mail className = "h-3.5 w-3.5" / > Messages < / TabsTrigger >
< TabsTrigger value = "threats" className = "gap-1.5 text-xs" > < Shield className = "h-3.5 w-3.5" / > Threats < / TabsTrigger >
< TabsTrigger value = "cloudusers" className = "gap-1.5 text-xs" > < Users className = "h-3.5 w-3.5" / > Cloud Users < / TabsTrigger >
< TabsTrigger value = "history" className = "gap-1.5 text-xs" > < Clock className = "h-3.5 w-3.5" / > History < / TabsTrigger >
< TabsTrigger value = "schedules" className = "gap-1.5 text-xs" > < Calendar className = "h-3.5 w-3.5" / > Schedules < / TabsTrigger >
2026-03-17 16:23:47 -04:00
< / TabsList >
2026-03-31 22:38:22 -04:00
< TabsContent value = "status" className = "mt-6" >
2026-03-17 16:23:47 -04:00
< StatusTab data = { statusData } onSync = { handleSync } syncing = { syncing } / >
< / TabsContent >
2026-03-31 22:38:22 -04:00
< TabsContent value = "held" className = "mt-6" > < HeldMailTab / > < / TabsContent >
feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
< TabsContent value = "delivered" className = "mt-6" > < DeliveredMailTab / > < / TabsContent >
2026-03-31 22:38:22 -04:00
< TabsContent value = "messages" className = "mt-6" > < MessagesTab / > < / TabsContent >
< TabsContent value = "threats" className = "mt-6" > < ThreatsTab / > < / TabsContent >
< TabsContent value = "cloudusers" className = "mt-6" > < CloudUserTab / > < / TabsContent >
< TabsContent value = "history" className = "mt-6" > < HistoryTab / > < / TabsContent >
< TabsContent value = "schedules" className = "mt-6" > < SyncScheduler / > < / TabsContent >
2026-03-17 16:23:47 -04:00
< / Tabs >
< / div >
) ;
}