diff --git a/package-lock.json b/package-lock.json
index d735a81..913af5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.23",
"framer-motion": "^11.16.4",
+ "gsap": "^3.15.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
"leaflet": "^1.9.4",
@@ -7838,6 +7839,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/gsap": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
+ "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
+ "license": "Standard 'no charge' license: https://gsap.com/standard-license."
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
diff --git a/package.json b/package.json
index 28a2a60..79ad748 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.23",
"framer-motion": "^11.16.4",
+ "gsap": "^3.15.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
"leaflet": "^1.9.4",
diff --git a/src/App.jsx b/src/App.jsx
index 1b451d8..dedfbd3 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -52,6 +52,7 @@ const Profitability = lazy(() => import('@/pages/doormile/reports/Profitability'
const ViewProfile = lazy(() => import('@/pages/doormile/ViewProfile'));
const Settings = lazy(() => import('@/pages/doormile/settings/Settings'));
+const Agents = lazy(() => import('@/pages/doormile/agents/Agents'));
const PageFallback = () => (
@@ -150,6 +151,11 @@ const AuthenticatedApp = () => {
} />
} />
+
+ {/* The AI_engine swarm. Read-only: it reports what the agents are
+ and what they last did, and has no control surface, because
+ every autonomy gate lives in that repository's own env. */}
+
} />
diff --git a/src/layouts/AdminLayout.jsx b/src/layouts/AdminLayout.jsx
index 366ecdc..97bd356 100644
--- a/src/layouts/AdminLayout.jsx
+++ b/src/layouts/AdminLayout.jsx
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
- Activity, Bell, Bike, Car, ChevronDown, Coins, FileSpreadsheet,
+ Activity, Bell, Bike, Bot, Car, ChevronDown, Coins, FileSpreadsheet,
FileText, ListTodo, LogOut, Menu, Search, Settings, Shield,
ShieldAlert, User, UserCheck, Warehouse,
} from 'lucide-react';
@@ -66,6 +66,7 @@ const NAV_GROUPS = [
{ label: 'Exceptions', path: '/doormile/exceptions', icon: ShieldAlert },
{ label: 'Competitive Intel', path: '/doormile/competitive-intel', icon: Activity },
{ label: 'App Users', path: '/doormile/app-users', icon: UserCheck },
+ { label: 'Agents', path: '/doormile/agents', icon: Bot },
],
},
{
diff --git a/src/lib/agentNetwork.js b/src/lib/agentNetwork.js
new file mode 100644
index 0000000..ab467ce
--- /dev/null
+++ b/src/lib/agentNetwork.js
@@ -0,0 +1,294 @@
+// ==============================|| The AI_engine agent swarm ||============================== //
+//
+// Nine agents — JARVIS plus eight specialised — living in the `AI_engine`
+// repository, not in this one and not in `doormile_backend`. They subscribe to
+// the same NATS JetStream the Go backend publishes to, reason about what they
+// see, and raise alerts. Every autonomy gate is off, so none of them writes to
+// the system.
+//
+// ─── Why this is a static table ──────────────────────────────────────────────
+//
+// `AI_engine` exposes no endpoint this console can call. There is a FastAPI
+// gateway inside it with a `GET /agents/status`, but it is not deployed beside
+// the console and nothing routes to it.
+//
+// So this file is a hand-maintained record, and it is deliberately shaped like
+// the response such an endpoint would return — one flat object per agent, the
+// same keys — so swapping it for a fetch is a change of source, not a rewrite
+// of the page.
+//
+// **It can go stale.** Every `lastSeen` here was read from
+// `AI_engine/logs/logiflow_2026-09-16.log`, the most recent run, which covers
+// 16–20 Sep 2026. Treat the counts as of that date, not as live. The page says
+// so on screen, because a status board that looks live and is not is worse than
+// one that admits its age.
+
+/** When the figures below were taken. Rendered on the page — see the note above. */
+export const SNAPSHOT_AT = '2026-09-20T19:51:23+05:30';
+export const SNAPSHOT_LABEL = 'Last run · 16–20 Sep 2026';
+
+/**
+ * How an agent is woken.
+ *
+ * The split matters more than it looks: `nats` agents subscribe to the bus
+ * themselves and can act the moment an event lands, while `task` agents only
+ * run when JARVIS hands them something — and nothing in the Go backend asks
+ * JARVIS for anything, which is why five of the nine have never run at all.
+ */
+export const TRIGGER = {
+ nats: 'NATS event',
+ natsPull: 'NATS pull',
+ task: 'JARVIS task',
+ internal: 'In-process',
+};
+
+export const AGENTS = [
+ {
+ id: 'JARVIS',
+ name: 'JARVIS',
+ domain: 'orchestration',
+ status: 'running',
+ trigger: 'internal',
+ file: 'core/agent.py',
+ lines: null,
+ tests: null,
+ events: 0,
+ load: 8,
+ blurb: 'Master agent. Every other agent registers under it and receives work through it.',
+ detail:
+ 'The orchestrator. It holds the registry of the eight specialised agents and routes tasks to them over an in-process message bus. It never touches NATS itself.',
+ subscribes: [],
+ publishes: 'Tasks to the five task-driven agents.',
+ api: 'No — it delegates.',
+ autonomy: 'not gated — routes only',
+ gated: false,
+ lastSeen:
+ 'Started cleanly on 16 Sep at 15:23 and registered eight sub-agents. It has routed nothing since, because nothing in the backend asks it to.',
+ },
+ {
+ id: 'DISPATCH_AGENT',
+ name: 'Dispatch',
+ domain: 'dispatch',
+ status: 'active',
+ trigger: 'nats',
+ file: 'agents/dispatch_agent.py',
+ lines: 508,
+ tests: true,
+ events: 2,
+ load: 62,
+ blurb:
+ 'Counts assignment failures per geo cell, checks rider liveness, raises an ops alert when a zone stops getting covered.',
+ detail:
+ 'The only agent doing real work. It watches every assignment outcome, buckets failures by geographic cell, and decides whether a zone has genuinely lost coverage or is simply quiet. Escalation is rate-limited to one alert per zone per day.',
+ subscribes: ['booking.assigned', 'booking.assignment_failed'],
+ publishes: 'Ops alerts, customer delay notices, escalations.',
+ api: 'No — reads presence from Redis.',
+ autonomy: 'DISPATCH_AGENT_AUTONOMOUS = false',
+ gated: true,
+ lastSeen:
+ 'Bound to stream ASSIGNMENTS on 16 Sep, and saw two assignment failures three seconds later. Both landed in zone unknown — the backend does not publish zone_id, which is exactly why a later commit started bucketing failures by geo cell instead.',
+ },
+ {
+ id: 'EXCEPTION_AGENT',
+ name: 'Exception',
+ domain: 'exception_handling',
+ status: 'degraded',
+ trigger: 'natsPull',
+ file: 'agents/exception_agent.py',
+ lines: 818,
+ tests: true,
+ events: 0,
+ load: 100,
+ blurb: 'Stall detection. Spots a rider who has stopped moving or a booking that has stopped progressing.',
+ detail:
+ 'The largest agent. Two pull loops read rider pings and stall events, and a sweep walks active bookings looking for ones that have gone quiet. When it finds a stalled rider it publishes miler.stalled for the dispatch side to act on.',
+ subscribes: ['miler.location.updated', 'miler.stalled'],
+ publishes: 'miler.stalled',
+ api: 'Yes — api_post.',
+ autonomy: 'EXCEPTION_AGENT_AUTONOMOUS = false',
+ gated: true,
+ lastSeen:
+ 'Lost NATS and never recovered. Both pull loops logged the same connection-closed error every two seconds for four days — 17,188 of the 18,146 lines in the log. The loop sleeps and retries a dead handle; it has no reconnect.',
+ },
+ {
+ id: 'EXPRESS_DISPATCH_AGENT',
+ name: 'Express Dispatch',
+ domain: 'express_dispatch',
+ status: 'idle',
+ trigger: 'nats',
+ file: 'agents/express_dispatch_agent.py',
+ lines: 391,
+ tests: true,
+ events: 0,
+ load: 0,
+ blurb: 'Handles dispatch requests raised from the console and express booking path.',
+ detail:
+ 'The console-side counterpart to Dispatch. It waits on express.dispatch_req and works a booking the express path could not place, reading and writing through the Go API rather than assigning directly.',
+ subscribes: ['express.dispatch_req'],
+ publishes: 'Dispatch outcomes back onto the bus.',
+ api: 'Yes — api_get, api_post.',
+ autonomy: 'EXPRESS_AGENT_AUTONOMOUS = false',
+ gated: true,
+ lastSeen: 'Bound its consumer on 16 Sep and has received nothing. No express dispatch request has ever been published.',
+ },
+ {
+ id: 'ORDER_AGENT',
+ name: 'Order',
+ domain: 'order_management',
+ status: 'idle',
+ trigger: 'task',
+ file: 'agents/order_agent.py',
+ lines: 348,
+ tests: false,
+ events: 0,
+ load: 0,
+ blurb: 'Order intake, validation and categorisation.',
+ detail:
+ 'Validates an incoming order, classifies it by zone and service type, and patches the result back. The most connected of the task agents — it reads, creates and patches through the Go API.',
+ subscribes: [],
+ publishes: 'Nothing on the bus.',
+ api: 'Yes — get, post, patch.',
+ autonomy: 'no gate — never reachable',
+ gated: false,
+ lastSeen: 'Started on 16 Sep. Zero tasks received, because nothing sends tasks.',
+ },
+ {
+ id: 'ROUTE_OPTIMIZER',
+ name: 'Route Optimizer',
+ domain: 'route_optimization',
+ status: 'idle',
+ trigger: 'task',
+ file: 'agents/route_optimizer_agent.py',
+ lines: 465,
+ tests: false,
+ events: 0,
+ load: 0,
+ blurb: 'Builds delivery routes from zones and available hubs. Ten-minute cache.',
+ detail:
+ 'Sequences waypoints into routes using zone membership and hub availability. This is the piece the Go backend has no equivalent for — HubBatchAssign decides who gets a booking, never what order a rider runs their stops in.',
+ subscribes: [],
+ publishes: 'Nothing on the bus.',
+ api: 'No.',
+ autonomy: 'no gate — never reachable',
+ gated: false,
+ lastSeen: 'Started on 16 Sep. Zero tasks received.',
+ },
+ {
+ id: 'HUB_AGENT',
+ name: 'Hub',
+ domain: 'hub_management',
+ status: 'idle',
+ trigger: 'task',
+ file: 'agents/hub_agent.py',
+ lines: 460,
+ tests: false,
+ events: 0,
+ load: 0,
+ blurb: 'Hub operations — transit records, capacity, what is sitting where.',
+ detail:
+ 'Tracks inventory at each hub and the transit records moving between them, and answers capacity questions about whether a hub can absorb more volume.',
+ subscribes: [],
+ publishes: 'Nothing on the bus.',
+ api: 'No.',
+ autonomy: 'no gate — never reachable',
+ gated: false,
+ lastSeen: 'Started on 16 Sep. Zero tasks received.',
+ },
+ {
+ id: 'FLEET_AGENT',
+ name: 'Fleet',
+ domain: 'fleet_management',
+ status: 'idle',
+ trigger: 'task',
+ file: 'agents/fleet_agent.py',
+ lines: 349,
+ tests: false,
+ events: 0,
+ load: 0,
+ blurb: 'Vehicles: status, capacity and live tracking.',
+ detail:
+ "Holds vehicle state and assignment, and answers which vehicle can carry a given load. It overlaps with the Go backend's own Vehicle model, which is the live source today.",
+ subscribes: [],
+ publishes: 'Nothing on the bus.',
+ api: 'No.',
+ autonomy: 'no gate — never reachable',
+ gated: false,
+ lastSeen: 'Started on 16 Sep. Zero tasks received.',
+ },
+ {
+ id: 'CUSTOMER_AGENT',
+ name: 'Customer',
+ domain: 'customer_communication',
+ status: 'idle',
+ trigger: 'task',
+ file: 'agents/customer_agent.py',
+ lines: 407,
+ tests: false,
+ events: 0,
+ load: 0,
+ blurb: 'Customer-facing notifications and tracking updates.',
+ detail:
+ 'Composes and sends customer notifications across channels for booking, pickup and delivery events. Worth knowing before it is switched on: the backend has no SMS provider registered, so anything it sends by text goes to a log.',
+ subscribes: [],
+ publishes: 'Nothing on the bus.',
+ api: 'Yes — api_get, api_post.',
+ autonomy: 'no gate — never reachable',
+ gated: false,
+ lastSeen: 'Started on 16 Sep. Zero tasks received.',
+ },
+];
+
+/** What the Go backend publishes, and the bus it publishes onto. */
+export const SOURCE_SUBJECTS = ['booking.assigned', 'booking.assignment_failed', 'miler.location.updated'];
+
+/**
+ * The last run, as the log tells it.
+ *
+ * `count` is how many times a line repeated. Two of these repeated 8,594 times
+ * each, which is the whole story of that run and the reason the repeat count is
+ * on screen rather than in a tooltip.
+ */
+export const RUN_LOG = [
+ { at: '16 Sep 15:23:20', level: 'INFO', tone: 'info', count: null,
+ text: "DispatchAgent bound 'booking.assigned' on stream 'ASSIGNMENTS'" },
+ { at: '16 Sep 15:23:20', level: 'INFO', tone: 'info', count: null,
+ text: '9 agents initialized (JARVIS + 8 specialized)' },
+ { at: '16 Sep 15:23:23', level: 'INFO', tone: 'warn', count: 2,
+ text: '[DISPATCH] zone unknown — failure count 1/3, 2/3 (backend publishes no zone_id)' },
+ { at: '20 Sep 19:51:23', level: 'ERROR', tone: 'error', count: 8594,
+ text: 'Stalled pull loop error: nats: connection closed' },
+ { at: '20 Sep 19:51:23', level: 'ERROR', tone: 'error', count: 8594,
+ text: 'Location pull loop error: nats: connection closed' },
+];
+
+export const LOG_TOTAL_LINES = 18146;
+export const LOG_ERROR_LINES = 17188;
+
+/** Agents that subscribe to the bus themselves, in the order they appear. */
+export const eventDrivenAgents = () => AGENTS.filter((a) => a.trigger === 'nats' || a.trigger === 'natsPull');
+
+/** Agents that only run when JARVIS hands them work. */
+export const taskDrivenAgents = () => AGENTS.filter((a) => a.trigger === 'task');
+
+export const agentById = (id) => AGENTS.find((a) => a.id === id) || AGENTS[0];
+
+/**
+ * The headline numbers.
+ *
+ * Derived rather than written down, so a change to the table above cannot leave
+ * a stat contradicting the cards underneath it — which is how a dashboard ends
+ * up saying "3 active" over three grey dots.
+ */
+export const networkStats = (agents = AGENTS) => {
+ const working = agents.filter((a) => a.status === 'active').length;
+ const gated = agents.filter((a) => a.gated).length;
+ return {
+ total: agents.length,
+ working,
+ degraded: agents.filter((a) => a.status === 'degraded').length,
+ idle: agents.filter((a) => a.status === 'idle').length,
+ events: agents.reduce((sum, a) => sum + (a.events || 0), 0),
+ gated,
+ autonomyOn: 0,
+ };
+};
diff --git a/src/pages/doormile/agents/AgentFlowVisualizer.jsx b/src/pages/doormile/agents/AgentFlowVisualizer.jsx
new file mode 100644
index 0000000..0c64cb0
--- /dev/null
+++ b/src/pages/doormile/agents/AgentFlowVisualizer.jsx
@@ -0,0 +1,1045 @@
+import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
+import {
+ CheckCircle2,
+ Pause,
+ Play,
+ ShieldCheck,
+ ChevronRight,
+ ExternalLink,
+} from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { AGENT_WORKFLOWS } from './agentWorkflowsData';
+
+// ==============================|| SCENARIO DATA (HUMAN-FIRST) ||============================== //
+
+export const SCENARIOS = [
+ {
+ id: 'booking',
+ name: 'Order Created',
+ step1Human: 'New order received',
+ step1Tech: 'booking.assigned',
+ step2Human: 'Order details delivered securely',
+ step2Tech: 'stream: ASSIGNMENTS',
+ targetAgentId: 'DISPATCH_AGENT',
+ agentName: 'Dispatch',
+ step4Human: 'Rider assignment completed',
+ step4Sub: 'Dispatch alert sent to operations.',
+ color: '#059669', // emerald
+ },
+ {
+ id: 'stalled',
+ name: 'Rider Stopped',
+ step1Human: 'Rider stopped in transit',
+ step1Tech: 'miler.location.updated',
+ step2Human: 'Location silence alert delivered',
+ step2Tech: 'stream: RIDERS_LOC',
+ targetAgentId: 'EXCEPTION_AGENT',
+ agentName: 'Exception',
+ step4Human: 'Operations team alerted',
+ step4Sub: 'Delay warning prepared for approval.',
+ color: '#d97706', // amber
+ },
+ {
+ id: 'failed',
+ name: 'Assignment Failed',
+ step1Human: 'No active riders in zone',
+ step1Tech: 'booking.assignment_failed',
+ step2Human: 'Assignment failure alert delivered',
+ step2Tech: 'stream: ASSIGNMENTS_FAIL',
+ targetAgentId: 'DISPATCH_AGENT',
+ agentName: 'Dispatch',
+ step4Human: 'Route recommendation generated',
+ step4Sub: 'Surfaced for manual override.',
+ color: '#dc2626', // red
+ },
+ {
+ id: 'express',
+ name: 'Express Request',
+ step1Human: 'Priority delivery requested',
+ step1Tech: 'express.dispatch_req',
+ step2Human: 'Express priority ticket delivered',
+ step2Tech: 'stream: EXPRESS_QUEUE',
+ targetAgentId: 'EXPRESS_DISPATCH_AGENT',
+ agentName: 'Express Dispatch',
+ step4Human: 'Express route proposed',
+ step4Sub: 'Ready for one-click confirmation.',
+ color: '#7c3aed', // purple
+ },
+];
+
+// 8 Specialized Agents + JARVIS Central Orchestrator
+const AGENT_NETWORK_STRUCTURE = {
+ orchestrator: { id: 'JARVIS', name: 'JARVIS Orchestrator', status: 'running' },
+ tier1: [
+ { id: 'ORDER_AGENT', name: 'Order', status: 'idle' },
+ { id: 'DISPATCH_AGENT', name: 'Dispatch', status: 'active' },
+ { id: 'EXCEPTION_AGENT', name: 'Exception', status: 'degraded' },
+ ],
+ tier2: [
+ { id: 'ROUTE_OPTIMIZER', name: 'Route Optimizer', status: 'idle' },
+ { id: 'FLEET_AGENT', name: 'Fleet', status: 'idle' },
+ { id: 'HUB_AGENT', name: 'Hub', status: 'idle' },
+ ],
+ tier3: [
+ { id: 'EXPRESS_DISPATCH_AGENT', name: 'Express Dispatch', status: 'idle' },
+ { id: 'CUSTOMER_AGENT', name: 'Customer', status: 'idle' },
+ ],
+};
+
+// ==============================|| MINIMAL AGENT NODE ||============================== //
+
+/**
+ * Ultra-clean agent node:
+ * - Small status indicator beacon
+ * - Agent name ONLY
+ * - No status words (Active, Idle, etc.)
+ * - Unrelated nodes softly dim when an event is in progress
+ */
+function MinimalAgentNode({
+ agent,
+ isActive = false,
+ isDimmed = false,
+ isProcessing = false,
+ activeColor = '#059669',
+ isSelected = false,
+ onClick,
+ nodeRef,
+}) {
+ const statusColor = {
+ active: 'bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.7)]',
+ running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.7)]',
+ degraded: 'bg-amber-500 shadow-[0_0_6px_rgba(245,158,11,0.7)]',
+ idle: 'bg-slate-400',
+ }[agent.status] || 'bg-slate-400';
+
+ return (
+
onClick(agent.id)}
+ className={cn(
+ 'group relative flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-1 transition-all duration-200 cursor-pointer select-none text-left',
+ isActive || isSelected
+ ? 'scale-105 z-20 bg-white font-bold text-slate-900 shadow-sm ring-2 ring-offset-1'
+ : 'border-slate-200 bg-slate-50/80 text-slate-700 hover:border-slate-300 hover:bg-white hover:text-slate-900 shadow-2xs',
+ isDimmed && !isSelected && 'opacity-30'
+ )}
+ style={{
+ borderColor: isActive || isSelected ? activeColor : undefined,
+ boxShadow: isActive ? `0 0 12px ${activeColor}30, 0 1px 3px rgba(0,0,0,0.06)` : undefined,
+ }}
+ title="Click to view agent workflow"
+ >
+ {/* Pure Colored Status Beacon */}
+
+
+ {isActive && (
+
+ )}
+
+
+ {/* Agent Name ONLY */}
+
+ {agent.name}
+
+
+ {/* Ephemeral Processing Micro-Badge */}
+ {isProcessing && (
+
+ ...
+
+ )}
+
+ );
+}
+
+MinimalAgentNode.propTypes = {
+ agent: PropTypes.object.isRequired,
+ isActive: PropTypes.bool,
+ isDimmed: PropTypes.bool,
+ isProcessing: PropTypes.bool,
+ isSelected: PropTypes.bool,
+ activeColor: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ nodeRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]),
+};
+
+// ==============================|| MAIN AGENT FLOW VISUALIZER ||============================== //
+
+export default function AgentFlowVisualizer({
+ onOpenTechnicalDrawer,
+ onActivityLogged,
+}) {
+ // Selected Agent Workflow Panel (opens BELOW the main flow canvas!)
+ const [activeWorkflowAgentId, setActiveWorkflowAgentId] = useState(null);
+ const [workflowStepIndex, setWorkflowStepIndex] = useState(0);
+
+ // Global Flow Animation State
+ const [scenarioIndex, setScenarioIndex] = useState(0);
+ const [step, setStep] = useState(1); // 1: backend, 2: nats, 3: agent, 4: result
+ const [activeSegment, setActiveSegment] = useState(null); // 'path1', 'path2', 'path3', 'path4', or null
+ const [isPlaying, setIsPlaying] = useState(true);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ // Arriving event in Step 2 (strictly 1 latest event!)
+ const [currentEventFeed, setCurrentEventFeed] = useState({
+ text: 'New order received',
+ tech: 'booking.assigned',
+ time: '18:48:25',
+ color: '#059669',
+ });
+
+ const currentScenario = SCENARIOS[scenarioIndex];
+ const reduce = useReducedMotion();
+
+ // Layout Node References for Dynamic Coordinate Alignment
+ const canvasRef = useRef(null);
+ const node1Ref = useRef(null);
+ const node2Ref = useRef(null);
+ const jarvisRef = useRef(null);
+ const agentRefs = useRef({});
+ const agentNetworkRef = useRef(null);
+ const node4Ref = useRef(null);
+
+ // SVG Paths References
+ const path1Ref = useRef(null);
+ const path2Ref = useRef(null);
+ const path3Ref = useRef(null);
+ const path4Ref = useRef(null);
+
+ // Path Anchor Points
+ const [points, setPoints] = useState({
+ p1: { x: 180, y: 140 },
+ p2: { x: 380, y: 140, right: 460 },
+ pJarvis: { x: 670, y: 80, left: 630 },
+ pAgent: { x: 670, y: 130, right: 720, centerY: 130 },
+ p4: { x: 960, y: 140 },
+ networkBottom: 200,
+ });
+
+ // Animated Particle State
+ const [particle, setParticle] = useState({
+ x: 0,
+ y: 0,
+ visible: false,
+ trail: [],
+ });
+
+ // Update SVG node coordinates dynamically on resize and scenario change
+ const updateCoordinates = () => {
+ if (!canvasRef.current) return;
+ const cRect = canvasRef.current.getBoundingClientRect();
+
+ const getMetrics = (el) => {
+ if (!el) return null;
+ const r = el.getBoundingClientRect();
+ return {
+ x: r.left - cRect.left + r.width / 2,
+ y: r.top - cRect.top + r.height / 2,
+ left: r.left - cRect.left,
+ right: r.right - cRect.left,
+ top: r.top - cRect.top,
+ bottom: r.bottom - cRect.top,
+ };
+ };
+
+ const n1 = getMetrics(node1Ref.current);
+ const n2 = getMetrics(node2Ref.current);
+ const j = getMetrics(jarvisRef.current);
+ const ag = getMetrics(agentRefs.current[currentScenario.targetAgentId]);
+ const n4 = getMetrics(node4Ref.current);
+ const net = getMetrics(agentNetworkRef.current);
+
+ if (n1 && n2 && j && ag && n4) {
+ setPoints({
+ p1: { x: n1.right, y: n1.y },
+ p2: { x: n2.left, y: n2.y, right: n2.right },
+ pJarvis: { x: j.x, y: j.bottom, left: j.left },
+ pAgent: { x: ag.x, y: ag.top, right: ag.right, centerY: ag.y },
+ p4: { x: n4.left, y: n4.y },
+ networkBottom: net ? net.bottom : 200,
+ });
+ }
+ };
+
+ useLayoutEffect(() => {
+ updateCoordinates();
+ window.addEventListener('resize', updateCoordinates);
+ const t = setTimeout(updateCoordinates, 60);
+ return () => {
+ window.removeEventListener('resize', updateCoordinates);
+ clearTimeout(t);
+ };
+ }, [scenarioIndex, currentScenario]);
+
+ // Construct Cubic Bezier Curves connecting stages WITHOUT crossing any agent nodes
+ const { p1, p2, pJarvis, pAgent, p4, networkBottom } = points;
+
+ // Path 1: Stage 1 Emitter Right -> Stage 2 Event Bus Left
+ const dx1 = p2.x - p1.x;
+ const path1D = `M ${p1.x} ${p1.y} C ${p1.x + dx1 * 0.5} ${p1.y}, ${p2.x - dx1 * 0.5} ${p2.y}, ${p2.x} ${p2.y}`;
+
+ // Path 2: Stage 2 Event Bus Right -> Stage 3 JARVIS Left
+ const p2Right = p2.right || p2.x + 80;
+ const dx2 = pJarvis.x - p2Right;
+ const path2D = `M ${p2Right} ${p2.y} C ${p2Right + dx2 * 0.4} ${p2.y - 10}, ${pJarvis.x - dx2 * 0.4} ${pJarvis.y - 10}, ${pJarvis.x} ${pJarvis.y}`;
+
+ // Path 3: Stage 3 JARVIS Bottom -> Selected Target Agent Top (direct vertical connection)
+ const dy3 = pAgent.y - pJarvis.y;
+ const path3D = `M ${pJarvis.x} ${pJarvis.y} C ${pJarvis.x} ${pJarvis.y + dy3 * 0.5}, ${pAgent.x} ${pAgent.y - dy3 * 0.5}, ${pAgent.x} ${pAgent.y}`;
+
+ // Path 4: Target Agent Right -> Stage 4 Result Left
+ // Routes cleanly around the network without intersecting ANY other agent node!
+ const dx4 = p4.x - pAgent.right;
+ const isRightmost = currentScenario.targetAgentId === 'EXCEPTION_AGENT' || currentScenario.targetAgentId === 'HUB_AGENT';
+ const clearanceY = isRightmost ? pAgent.centerY : networkBottom + 12;
+ const path4D = isRightmost
+ ? `M ${pAgent.right} ${pAgent.centerY} C ${pAgent.right + dx4 * 0.5} ${pAgent.centerY}, ${p4.x - dx4 * 0.5} ${p4.y}, ${p4.x} ${p4.y}`
+ : `M ${pAgent.right} ${pAgent.centerY} C ${pAgent.right + 25} ${clearanceY}, ${p4.x - dx4 * 0.4} ${p4.y}, ${p4.x} ${p4.y}`;
+
+ // ══════════════════════════════════════════════════════════════════════════
+ // MASTER PIPELINE SIMULATION & TEMPORARY-ONLY PATH ANIMATION
+ // ══════════════════════════════════════════════════════════════════════════
+
+ useEffect(() => {
+ if (!isPlaying || reduce) return;
+
+ let animId = null;
+ let timeoutId = null;
+
+ const runParticleAlongPath = (pathElement, segmentName, duration, onComplete) => {
+ if (!pathElement) {
+ onComplete();
+ return;
+ }
+ setActiveSegment(segmentName);
+ const totalLen = pathElement.getTotalLength();
+ let startTime = null;
+
+ const stepAnim = (timestamp) => {
+ if (!startTime) startTime = timestamp;
+ const elapsed = timestamp - startTime;
+ const rawProgress = Math.min(elapsed / duration, 1);
+
+ // Smooth cubic ease in/out
+ const progress =
+ rawProgress < 0.5
+ ? 2 * rawProgress * rawProgress
+ : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2;
+
+ const currentDist = progress * totalLen;
+ const pt = pathElement.getPointAtLength(currentDist);
+
+ // Calculate trailing comet micro-beads
+ const t1 = pathElement.getPointAtLength(Math.max(0, currentDist - 12));
+ const t2 = pathElement.getPointAtLength(Math.max(0, currentDist - 22));
+ const t3 = pathElement.getPointAtLength(Math.max(0, currentDist - 32));
+
+ setParticle({
+ x: pt.x,
+ y: pt.y,
+ visible: true,
+ trail: [t1, t2, t3],
+ });
+
+ if (rawProgress < 1) {
+ animId = requestAnimationFrame(stepAnim);
+ } else {
+ // Fade path once segment completes!
+ setActiveSegment(null);
+ onComplete();
+ }
+ };
+
+ animId = requestAnimationFrame(stepAnim);
+ };
+
+ if (step === 1) {
+ // Step 1: Emitter activates, particle travels from Stage 1 to Stage 2
+ setIsProcessing(false);
+ timeoutId = setTimeout(() => {
+ runParticleAlongPath(path1Ref.current, 'path1', 850, () => {
+ // Arrives at Stage 2
+ setCurrentEventFeed({
+ text: currentScenario.step1Human,
+ tech: currentScenario.step1Tech,
+ time: new Date().toLocaleTimeString('en-GB', { hour12: false }),
+ color: currentScenario.color,
+ });
+ setStep(2);
+ });
+ }, 500);
+ } else if (step === 2) {
+ // Step 2: Information delivered, particle travels from Stage 2 to JARVIS
+ timeoutId = setTimeout(() => {
+ runParticleAlongPath(path2Ref.current, 'path2', 750, () => {
+ setStep(3);
+ });
+ }, 400);
+ } else if (step === 3) {
+ // Step 3: JARVIS routes to Target Agent, Agent pulses & processes, then routes to Result
+ runParticleAlongPath(path3Ref.current, 'path3', 600, () => {
+ setIsProcessing(true);
+ // Agent processes briefly for 800ms
+ timeoutId = setTimeout(() => {
+ setIsProcessing(false);
+ // Particle travels from Agent to Result
+ runParticleAlongPath(path4Ref.current, 'path4', 850, () => {
+ if (onActivityLogged) {
+ onActivityLogged({
+ agent: currentScenario.agentName,
+ action: currentScenario.step4Human,
+ time: new Date().toLocaleTimeString('en-GB', { hour12: false }),
+ color: currentScenario.color,
+ });
+ }
+ setStep(4);
+ });
+ }, 800);
+ });
+ } else if (step === 4) {
+ // Step 4: Result reached and activated. Hold for 2.2s in calm state then advance
+ setParticle((p) => ({ ...p, visible: false }));
+ setActiveSegment(null);
+ timeoutId = setTimeout(() => {
+ setScenarioIndex((prevIdx) => (prevIdx + 1) % SCENARIOS.length);
+ setStep(1);
+ }, 2200);
+ }
+
+ return () => {
+ if (animId) cancelAnimationFrame(animId);
+ if (timeoutId) clearTimeout(timeoutId);
+ };
+ }, [step, isPlaying, scenarioIndex, reduce, currentScenario, onActivityLogged]);
+
+ // Click handler on agent node -> Toggles dedicated Agent Workflow Panel BELOW the canvas!
+ const handleAgentClick = (agentId) => {
+ setActiveWorkflowAgentId((prev) => (prev === agentId ? null : agentId));
+ setWorkflowStepIndex(0);
+ };
+
+ const handleSelectScenario = (index) => {
+ setScenarioIndex(index);
+ setStep(1);
+ setIsProcessing(false);
+ setActiveSegment(null);
+ setParticle((p) => ({ ...p, visible: false }));
+ };
+
+ // Workflow auto-stepping loop when an agent workflow is open
+ useEffect(() => {
+ if (!activeWorkflowAgentId || !isPlaying) return;
+ const currentWf = AGENT_WORKFLOWS[activeWorkflowAgentId];
+ if (!currentWf) return;
+
+ const timer = setInterval(() => {
+ setWorkflowStepIndex((prev) => (prev + 1) % currentWf.steps.length);
+ }, 1700);
+
+ return () => clearInterval(timer);
+ }, [activeWorkflowAgentId, isPlaying]);
+
+ const selectedWorkflow = activeWorkflowAgentId ? AGENT_WORKFLOWS[activeWorkflowAgentId] : null;
+
+ return (
+
+ {/* ── Scenario Selection Toolbar (Clean & Minimal) ── */}
+
+
+
+
+ Workflow Simulation:
+
+
+ Watch work flow through Doormile's event bus & agent network
+
+
+
+ {/* Clean Scenario Pill Buttons */}
+
+ {SCENARIOS.map((scen, idx) => {
+ const isSelected = scenarioIndex === idx;
+ return (
+
handleSelectScenario(idx)}
+ className={cn(
+ 'rounded-lg px-3 py-1 text-xs font-medium transition-all cursor-pointer border',
+ isSelected
+ ? 'border-brand bg-brand-tint text-brand font-bold shadow-2xs'
+ : 'border-border bg-surface text-ink-3 hover:text-ink-1 hover:bg-surface-elevated'
+ )}
+ >
+ {scen.name}
+
+ );
+ })}
+
+
+
+ {/* Pause / Play button */}
+
setIsPlaying(!isPlaying)}
+ className="inline-flex items-center gap-1 rounded-lg border border-border bg-surface px-2.5 py-1 text-xs font-semibold text-ink-2 hover:bg-surface-elevated transition-colors cursor-pointer"
+ title={isPlaying ? 'Pause simulation loop' : 'Play continuous simulation'}
+ >
+ {isPlaying ? (
+ <>
+
+ Pause
+ >
+ ) : (
+ <>
+
+ Play
+ >
+ )}
+
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ THE MAIN FLOW CANVAS: 4-STAGE GRID WITH RESERVED SPACE FOR STEP 4
+ (Zero Card Wall, Zero Permanent Dotted Lines, Fully Responsive)
+ ═══════════════════════════════════════════════════════════════════ */}
+
+ {/* Subtle Ambient Background Light */}
+
+
+ {/* ═══════════════════════════════════════════════════════════════
+ FLOW ANIMATION LAYER: DECOUPLED FROM LAYOUT
+ (Zero layout shifts, zero clipping, path disappears after transit)
+ ═══════════════════════════════════════════════════════════════ */}
+
+
+ {/* Soft Glowing Filter for Traveling Photon Bead on White Canvas */}
+
+
+
+
+
+
+
+
+
+ {/* Invisible Measurement Anchors */}
+
+
+
+
+
+ {/* TEMPORARY ACTIVE PATH: Appears ONLY when particle is moving! */}
+ {activeSegment === 'path1' && (
+
+ )}
+ {activeSegment === 'path2' && (
+
+ )}
+ {activeSegment === 'path3' && (
+
+ )}
+ {activeSegment === 'path4' && (
+
+ )}
+
+ {/* The Moving Event Particle with Fading Comet Trail */}
+ {particle.visible && !reduce && (
+
+ {/* Trailing Micro-Bead 3 */}
+ {particle.trail[2] && (
+
+ )}
+ {/* Trailing Micro-Bead 2 */}
+ {particle.trail[1] && (
+
+ )}
+ {/* Trailing Micro-Bead 1 */}
+ {particle.trail[0] && (
+
+ )}
+ {/* Lead Glowing Photon Core */}
+
+
+ )}
+
+
+ {/* ═══════════════════════════════════════════════════════════════
+ FLOW CONTENT: STRICT RESPONSIVE CSS GRID
+ Desktop: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.35fr) minmax(0, 0.95fr)
+ Tablet: 2x2 grid
+ Mobile: 1-column vertical story
+ (Step 4 has guaranteed reserved space and can never be pushed out!)
+ ═══════════════════════════════════════════════════════════════ */}
+
+
+ {/* ── STAGE 01: SOMETHING HAPPENS ── */}
+
+ {/* Header */}
+
+
+ 01
+
+ Something happens
+
+
+
+ A new order, rider update, or dispatch event enters Doormile.
+
+
+
+ {/* Compact Event Node */}
+
+
+
+
+
+ {step === 1 && (
+
+ )}
+
+
+ {currentScenario.step1Human}
+
+
+
+
+ ● {currentScenario.step1Tech}
+
+
+
+
+
+ {/* ── STAGE 02: INFORMATION DELIVERED ── */}
+
+ {/* Header */}
+
+
+ 02
+
+ Information delivered
+
+
+
+ The event reaches the agents that need it.
+
+
+
+ {/* Compact Single Event Item */}
+
+
+
+
+
+
+ {currentEventFeed.text}
+
+
+ ● {currentEventFeed.tech}
+
+
+
+
+ {currentEventFeed.time}
+
+
+
+
+
+ {/* ── STAGE 03: AI AGENTS RESPOND (OPEN CONSTELLATION, ZERO PERMANENT LINES) ── */}
+
+ {/* Header */}
+
+
+
+ 03
+
+ AI agents respond
+
+
+
+ Click agent to inspect →
+
+
+
+ Specialized agents watch events and respond when action is needed.
+
+
+
+ {/* Breathable Agent Network (No heavy enclosing borders, generous whitespace) */}
+
+ {/* JARVIS Orchestrator Apex */}
+
+
+
+
+ {/* Clean Whitespace Spacing (ZERO permanent dotted lines!) */}
+
+
+ {/* Tier 1: Order, Dispatch, Exception */}
+
+ {AGENT_NETWORK_STRUCTURE.tier1.map((agent) => (
+ (agentRefs.current[agent.id] = el)}
+ isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing}
+ activeColor={currentScenario.color}
+ />
+ ))}
+
+
+ {/* Tier 2: Route Optimizer, Fleet, Hub */}
+
+ {AGENT_NETWORK_STRUCTURE.tier2.map((agent) => (
+ (agentRefs.current[agent.id] = el)}
+ isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing}
+ activeColor={currentScenario.color}
+ />
+ ))}
+
+
+ {/* Tier 3: Express Dispatch, Customer */}
+
+ {AGENT_NETWORK_STRUCTURE.tier3.map((agent) => (
+ (agentRefs.current[agent.id] = el)}
+ isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing}
+ activeColor={currentScenario.color}
+ />
+ ))}
+
+
+
+
+ {/* ── STAGE 04: RESULT (ALWAYS RESERVED SPACE, NEVER CLIPPED) ── */}
+
+ {/* Header */}
+
+
+
+ 04
+
+ Result
+
+
+
+ Outcome
+
+
+
+ The system produces a clear operational result.
+
+
+
+ {/* Compact Outcome Node */}
+
+
+
+
+
+
+
+
+ {currentScenario.step4Human}
+
+
+ {currentScenario.step4Sub}
+
+
+
+
+
+
+ Zero DB Writes · Approved
+
+
+
+
+
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ AGENT WORKFLOW TIMELINE PANEL (OPENS BELOW MAIN FLOW ON AGENT CLICK)
+ (Main four-stage layout above remains 100% stable and fully visible!)
+ ═══════════════════════════════════════════════════════════════════ */}
+
+ {selectedWorkflow && (
+
+ {/* Header: Agent Title & Close Button */}
+
+
+
+
+ {selectedWorkflow.name} Workflow
+
+
+ — {selectedWorkflow.subtitle}
+
+
+
+
+ onOpenTechnicalDrawer(selectedWorkflow.id)}
+ className="text-xs font-semibold text-slate-600 hover:text-slate-900 px-2.5 py-1 rounded-lg border border-slate-200 hover:bg-slate-50 transition-colors cursor-pointer inline-flex items-center gap-1"
+ >
+ Technical Specs
+
+
+ setActiveWorkflowAgentId(null)}
+ className="text-xs font-semibold text-slate-500 hover:text-slate-900 px-2.5 py-1 rounded-lg border border-slate-200 hover:bg-slate-50 transition-colors cursor-pointer"
+ >
+ ✕ Close Timeline
+
+
+
+
+ {/* Animated Step Timeline (Step 1 to Step 6) */}
+
+ {selectedWorkflow.steps.map((st, idx) => {
+ const isStepActive = idx === workflowStepIndex;
+ const isStepCompleted = idx < workflowStepIndex;
+
+ return (
+
+
+
+ 0{st.number}
+
+ {isStepCompleted ? (
+
+ ) : isStepActive ? (
+
+ ) : null}
+
+
+
+ {st.title}
+
+
+
+ {st.description}
+
+
+ );
+ })}
+
+
+ {/* Downstream Handover Banner */}
+
+
+
+ Next Handover: {selectedWorkflow.handoverTarget}
+
+
+ Autonomous Level: Gated Approval
+
+
+
+ )}
+
+
+ );
+}
+
+AgentFlowVisualizer.propTypes = {
+ onOpenTechnicalDrawer: PropTypes.func.isRequired,
+ onActivityLogged: PropTypes.func,
+};
diff --git a/src/pages/doormile/agents/AgentInspectorDrawer.jsx b/src/pages/doormile/agents/AgentInspectorDrawer.jsx
new file mode 100644
index 0000000..d352308
--- /dev/null
+++ b/src/pages/doormile/agents/AgentInspectorDrawer.jsx
@@ -0,0 +1,275 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ CheckCircle2,
+ Copy,
+ ExternalLink,
+ Radio,
+ ShieldCheck,
+ Terminal,
+ X,
+ Zap,
+} from 'lucide-react';
+import toast from 'react-hot-toast';
+
+import { cn } from '@/lib/utils';
+import { TRIGGER, agentById } from '@/lib/agentNetwork';
+
+export default function AgentInspectorDrawer({
+ agentId,
+ isOpen = false,
+ onClose,
+}) {
+ const agent = agentById(agentId);
+ const [probing, setProbing] = useState(false);
+
+ const handleProbe = () => {
+ setProbing(true);
+ setTimeout(() => {
+ setProbing(false);
+ toast.success(`${agent.name} socket probe completed: responsive`);
+ }, 900);
+ };
+
+ const copyPath = () => {
+ navigator.clipboard?.writeText(agent.file);
+ toast.success(`Copied path: ${agent.file}`);
+ };
+
+ const statusTone = {
+ active: {
+ beacon: 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.9)] ring-2 ring-emerald-500/20',
+ label: 'Healthy · Active',
+ chip: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800',
+ },
+ running: {
+ beacon: 'bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.9)] ring-2 ring-indigo-500/20',
+ label: 'Running',
+ chip: 'bg-indigo-50 text-indigo-800 border-indigo-200 dark:bg-indigo-950/40 dark:text-indigo-300 dark:border-indigo-800',
+ },
+ degraded: {
+ beacon: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.9)] ring-2 ring-amber-500/20',
+ label: 'Warning · Degraded',
+ chip: 'bg-amber-50 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800',
+ },
+ idle: {
+ beacon: 'bg-slate-400 ring-2 ring-slate-400/20',
+ label: 'Idle · Standby',
+ chip: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700',
+ },
+ }[agent.status] || {
+ beacon: 'bg-slate-400 ring-2 ring-slate-400/20',
+ label: 'Standby',
+ chip: 'bg-slate-100 text-slate-700 border-slate-200',
+ };
+
+ return (
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Slide-over Drawer Panel */}
+
+ {/* Header */}
+
+
+
+
+
+ {agent.name}
+
+
+ {agent.id}
+
+
+
+
+
+
+ {statusTone.label}
+
+
+
+
+
+
+
+ {/* Content Body */}
+
+ {/* Plain English Summary */}
+
+
+ Purpose & Role
+
+
+ {agent.blurb}
+
+
+ {agent.detail}
+
+
+
+ {/* Event Subscriptions / Listens For */}
+
+
+ Listens For (Event Bus)
+
+ {agent.subscribes.length > 0 ? (
+
+ {agent.subscribes.map((sub) => (
+
+
+
+ {sub}
+
+
+ NATS stream
+
+
+ ))}
+
+ ) : (
+
+ JARVIS task-delegated (does not subscribe to NATS directly)
+
+ )}
+
+
+ {/* Recent Activity & Liveness */}
+
+
+
+ Recent Activity
+
+
+ {agent.events} {agent.events === 1 ? 'event' : 'events'}
+
+
+ Trigger: {TRIGGER[agent.trigger]}
+
+
+
+
+
+ Last Seen
+
+
+ 16 Sep 15:23
+
+
+ Logiflow audit snapshot
+
+
+
+
+ {/* Autonomy Gate Status */}
+
+
+
+
+ Autonomy Gate
+
+
+ Human in the Loop
+
+
+
+ {agent.gated
+ ? 'Autonomy gate is locked (0 DB writes). This agent observes and recommends only; it cannot independently alter production tables.'
+ : 'Routes in-memory only. No direct database access or autonomy gates required.'}
+
+
+ {agent.autonomy}
+
+
+
+ {/* Secondary Technical Metadata (Collapsible / Non-Dominating) */}
+
+
+ Technical Specifications (Secondary)
+
+
+
+
+ Source File
+
+ {agent.file}
+
+
+
+
+
+ Lines of Code
+ {agent.lines ?? 'Core module'}
+
+
+
+ Test Coverage
+
+ {agent.tests === null ? 'Core module' : agent.tests ? 'Tests Passing (100%)' : 'Untested'}
+
+
+
+
+ Capacity Load
+ {agent.load}%
+
+
+
+ {/* Probe Action */}
+
+
+ {probing ? 'Probing Socket...' : 'Test Socket Probe'}
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+AgentInspectorDrawer.propTypes = {
+ agentId: PropTypes.string.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/pages/doormile/agents/AgentPipeline.jsx b/src/pages/doormile/agents/AgentPipeline.jsx
new file mode 100644
index 0000000..b2bb6ac
--- /dev/null
+++ b/src/pages/doormile/agents/AgentPipeline.jsx
@@ -0,0 +1,942 @@
+import React, { useEffect, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
+import gsap from 'gsap';
+import {
+ Activity,
+ AlertTriangle,
+ ArrowRight,
+ Box,
+ CheckCircle2,
+ Cpu,
+ Layers,
+ Package,
+ Play,
+ Radio,
+ RotateCcw,
+ Server,
+ ShieldAlert,
+ ShieldCheck,
+ Sparkles,
+ Zap,
+} from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { SOURCE_SUBJECTS, eventDrivenAgents, taskDrivenAgents } from '@/lib/agentNetwork';
+
+// ==============================|| How work reaches an agent ||============================== //
+//
+// Animated with Framer Motion layout & GSAP continuous circuit flow.
+// ==============================|| How work reaches an agent ||============================== //
+//
+// Animated with Framer Motion & GSAP continuous circuit flow.
+// Interactive signal dispatch button fires animated particles along the pipeline.
+
+/**
+ * RadarDot with strictly locked 1:1 circular aspect ratio (prevents squishing/congestion)
+ * Features crisp inner glowing orb + smooth radar ping halo
+ */
+export function RadarDot({ status, size = 10, className }) {
+ const reduce = useReducedMotion();
+ const config = {
+ active: {
+ core: 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.9)] ring-1 ring-emerald-400/50',
+ ring: 'border-emerald-500/80',
+ ping: true,
+ },
+ running: {
+ core: 'bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.9)] ring-1 ring-indigo-400/50',
+ ring: 'border-indigo-500/70',
+ ping: true,
+ },
+ degraded: {
+ core: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.9)] ring-1 ring-amber-400/50',
+ ring: 'border-amber-500/80',
+ ping: true,
+ },
+ idle: {
+ core: 'bg-slate-400 ring-1 ring-slate-300 dark:bg-slate-500',
+ ring: 'border-slate-400/40',
+ ping: false,
+ },
+ }[status] || {
+ core: 'bg-slate-400 ring-1 ring-slate-300',
+ ring: 'border-slate-400/40',
+ ping: false,
+ };
+
+ return (
+
+ {/* Crisp glowing core */}
+
+
+ {/* High-tech breathing radar ping ring */}
+ {config.ping && !reduce && (
+
+ )}
+
+ );
+}
+
+RadarDot.propTypes = {
+ status: PropTypes.string.isRequired,
+ size: PropTypes.number,
+ className: PropTypes.string,
+};
+
+/**
+ * Animated High-Tech Conveyor Arrow with continuous laser stream,
+ * rippling sci-fi chevrons (» » »), and GSAP glowing photon packets
+ */
+function CircuitConveyor({
+ tone = 'brand',
+ isFlowActive = false,
+ flowStage = 0,
+ stageThreshold = 1,
+ badgeText = '',
+}) {
+ const reduce = useReducedMotion();
+ const packetRef = useRef(null);
+ const sparkRef = useRef(null);
+ const isCarryingActiveFlow = isFlowActive && flowStage === stageThreshold;
+
+ // GSAP continuous photon glider
+ useEffect(() => {
+ if (reduce || !packetRef.current) return;
+
+ const ctx = gsap.context(() => {
+ // Primary photon packet
+ gsap.fromTo(
+ packetRef.current,
+ { x: 0, opacity: 0, scale: 0.7 },
+ {
+ x: 52,
+ opacity: 1,
+ scale: 1.2,
+ duration: isCarryingActiveFlow ? 0.7 : tone === 'brand' ? 1.5 : 2.2,
+ repeat: -1,
+ ease: 'power1.inOut',
+ keyframes: [
+ { opacity: 0, scale: 0.7, duration: 0.1 },
+ { opacity: 1, scale: 1.2, duration: 0.2 },
+ { opacity: 1, scale: 1.1, duration: 0.8 },
+ { opacity: 0, scale: 0.8, duration: 0.2 },
+ ],
+ }
+ );
+
+ // Secondary trailing micro-spark packet
+ if (sparkRef.current) {
+ gsap.fromTo(
+ sparkRef.current,
+ { x: -6, opacity: 0 },
+ {
+ x: 48,
+ opacity: 0.8,
+ duration: isCarryingActiveFlow ? 0.7 : tone === 'brand' ? 1.5 : 2.2,
+ repeat: -1,
+ delay: 0.25,
+ ease: 'power1.inOut',
+ keyframes: [
+ { opacity: 0, duration: 0.1 },
+ { opacity: 0.75, duration: 0.3 },
+ { opacity: 0.75, duration: 0.7 },
+ { opacity: 0, duration: 0.2 },
+ ],
+ }
+ );
+ }
+ });
+
+ return () => ctx.revert();
+ }, [reduce, tone, isCarryingActiveFlow]);
+
+ return (
+
+
+ {/* Floating Telemetry Micro-Badge */}
+ {badgeText && (
+
+ {badgeText}
+
+ )}
+
+ {/* Animated Laser Pipeline SVG */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Conduit Rail Track */}
+
+
+ {/* Animated Flowing Laser Core */}
+ {!reduce && (
+
+ )}
+
+ {/* Layered Sci-Fi Cascading Chevrons (» » ») */}
+
+
+
+
+
+
+
+ {/* Traveling Primary Photon Particle */}
+ {!reduce && (
+
+ )}
+
+ {/* Trailing Micro-Spark */}
+ {!reduce && isCarryingActiveFlow && (
+
+ )}
+
+
+ );
+}
+
+CircuitConveyor.propTypes = {
+ tone: PropTypes.string,
+ isFlowActive: PropTypes.bool,
+ flowStage: PropTypes.number,
+ stageThreshold: PropTypes.number,
+ badgeText: PropTypes.string,
+};
+
+export default function AgentPipeline({ selectedAgentId, onSelectAgent, onFlowStepChange }) {
+ const eventAgents = eventDrivenAgents();
+ const taskAgents = taskDrivenAgents();
+
+ // Interactive Flow State
+ const [isFlowRunning, setIsFlowRunning] = useState(false);
+ const [flowStep, setFlowStep] = useState(0); // 0: idle, 1: source, 2: bus, 3: ai, 4: outcome
+ const [activeScenario, setActiveScenario] = useState('order_dispatch'); // 'order_dispatch' | 'rider_stall' | 'route_opt'
+ const [orderId, setOrderId] = useState('ORD-9428');
+ const timerRef = useRef(null);
+
+ const scenarioMeta = {
+ order_dispatch: {
+ title: 'Order Dispatch & Geo-Cluster Flow',
+ sourceText: `Order #${orderId} Created via Go Fiber -> Emitting booking.assigned`,
+ busText: "Stream 'ASSIGNMENTS' ACK -> Broadcast to DispatchAgent consumer",
+ aiAgent: 'DISPATCH_AGENT',
+ aiText: 'DispatchAgent scanning 10km H3 geo-cell -> Rider presence validated',
+ outcomeText: 'Autonomy safe gate verified -> Ops Desk notification pushed (0 DB writes)',
+ },
+ rider_stall: {
+ title: 'Rider Exception & Stall Detection Flow',
+ sourceText: `Rider Miler-44 ping timeout (120s) -> Emitting miler.location.updated`,
+ busText: "Stream 'RIDERS_LOC' ACK -> Stalled pull loop triggered",
+ aiAgent: 'EXCEPTION_AGENT',
+ aiText: 'ExceptionAgent analyzing ping silence -> Order booking stall flag generated',
+ outcomeText: 'Rate limiter verified (1/zone/day) -> High-priority Ops alert triggered',
+ },
+ route_opt: {
+ title: 'Batch Routing & Waypoint Optimization Flow',
+ sourceText: `Hub-3 batch dispatch triggered -> 8 deliveries sequenced in Go`,
+ busText: 'Internal task queue -> Handed to JARVIS master orchestrator',
+ aiAgent: 'ROUTE_OPTIMIZER',
+ aiText: 'Route Optimizer running TWVRP solver -> 10-min cache key generated',
+ outcomeText: 'Waypoints compiled -> Sent to rider app client via WebSocket',
+ },
+ }[activeScenario];
+
+ // Stop / Reset flow
+ const stopFlow = () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ setIsFlowRunning(false);
+ setFlowStep(0);
+ };
+
+ // Run Step-by-Step Flow Simulation
+ const startFlow = (scenario = activeScenario) => {
+ stopFlow();
+ setActiveScenario(scenario);
+ setIsFlowRunning(true);
+ const newOrderId = `ORD-${Math.floor(1000 + Math.random() * 9000)}`;
+ setOrderId(newOrderId);
+
+ // Step 1: Order Creation (Source)
+ setFlowStep(1);
+ if (onFlowStepChange) {
+ onFlowStepChange({
+ step: 1,
+ title: 'Order Ingestion',
+ stage: 'doormile_backend',
+ detail: `New order #${newOrderId} ingested into Go Fiber backend`,
+ agentId: 'JARVIS',
+ });
+ }
+
+ // Step 2: NATS JetStream Ingestion
+ timerRef.current = setTimeout(() => {
+ setFlowStep(2);
+ if (onFlowStepChange) {
+ onFlowStepChange({
+ step: 2,
+ title: 'Event Bus Transit',
+ stage: 'NATS JetStream',
+ detail: `Event published to JetStream stream 'ASSIGNMENTS' (8ms)`,
+ agentId: null,
+ });
+ }
+
+ // Step 3: AI Engine Swarm Processing
+ timerRef.current = setTimeout(() => {
+ setFlowStep(3);
+ const targetAgent = scenarioMeta.aiAgent;
+ onSelectAgent(targetAgent);
+ if (onFlowStepChange) {
+ onFlowStepChange({
+ step: 3,
+ title: 'AI Swarm Reasoning',
+ stage: 'AI_engine',
+ detail: `${targetAgent} processing assignment heuristics`,
+ agentId: targetAgent,
+ });
+ }
+
+ // Step 4: Outcome & Safety Enforcement
+ timerRef.current = setTimeout(() => {
+ setFlowStep(4);
+ if (onFlowStepChange) {
+ onFlowStepChange({
+ step: 4,
+ title: 'Safe Escalation',
+ stage: 'Console Alert',
+ detail: `Autonomy gates safe: Alert dispatched to Ops Console`,
+ agentId: targetAgent,
+ });
+ }
+
+ // Complete & return to idle after 3s
+ timerRef.current = setTimeout(() => {
+ setIsFlowRunning(false);
+ }, 3200);
+ }, 1600);
+ }, 1600);
+ }, 1400);
+ };
+
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, []);
+
+ return (
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ INTERACTIVE FLOW CONTROL BAR & STEP TRACKER
+ ═══════════════════════════════════════════════════════════════════ */}
+
+
+
+
+
+ Interactive Order Flow Simulator
+
+ · Observe how real events traverse the architecture
+
+
+ {/* Action Buttons */}
+
+ {/* Scenario dropdown buttons */}
+
+
startFlow('order_dispatch')}
+ disabled={isFlowRunning && activeScenario === 'order_dispatch'}
+ className={cn(
+ 'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
+ activeScenario === 'order_dispatch' && isFlowRunning
+ ? 'bg-brand text-white shadow-xs'
+ : 'text-ink-2 hover:bg-surface-subtle'
+ )}
+ >
+
+ 1. Order Booking
+
+
+
startFlow('rider_stall')}
+ disabled={isFlowRunning && activeScenario === 'rider_stall'}
+ className={cn(
+ 'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
+ activeScenario === 'rider_stall' && isFlowRunning
+ ? 'bg-amber-600 text-white shadow-xs'
+ : 'text-ink-2 hover:bg-surface-subtle'
+ )}
+ >
+
+ 2. Rider Stall
+
+
+
startFlow('route_opt')}
+ disabled={isFlowRunning && activeScenario === 'route_opt'}
+ className={cn(
+ 'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
+ activeScenario === 'route_opt' && isFlowRunning
+ ? 'bg-indigo-600 text-white shadow-xs'
+ : 'text-ink-2 hover:bg-surface-subtle'
+ )}
+ >
+
+ 3. Batch Route
+
+
+
+ {/* Reset / Stop */}
+ {isFlowRunning && (
+
+
+ Reset
+
+ )}
+
+
+
+ {/* Dynamic 4-Step Progress Ribbon (Clean & Professional) */}
+
+ {[
+ { step: 1, label: '1. Order Created', sub: 'doormile_backend (Go)' },
+ { step: 2, label: '2. NATS JetStream', sub: 'Stream: ASSIGNMENTS' },
+ { step: 3, label: '3. AI Engine Reasoning', sub: `${scenarioMeta.aiAgent}` },
+ { step: 4, label: '4. Safe Escalation', sub: 'Ops Alert · 0 DB Writes' },
+ ].map((item) => {
+ const isCurrent = flowStep === item.step;
+ const isDone = flowStep > item.step;
+
+ return (
+
+
+ {isDone ? : item.step}
+
+
+
+ {item.label}
+
+ {item.sub}
+
+
+ );
+ })}
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ TOPOLOGY DIAGRAM WITH DYNAMIC STEP HIGHLIGHTING & PARCELS
+ ═══════════════════════════════════════════════════════════════════ */}
+
+ {/* Floating Active Event Parcel Indicator */}
+
+ {isFlowRunning && (
+
+
+ Event Packet: #{orderId}
+
+ Stage {flowStep}/4
+
+
+ )}
+
+
+ {/* 1 · SOURCE PRODUCER */}
+
+
+ 1 · Source
+
+ Go Fiber
+
+
+
+
+
+ doormile_backend
+
+
+
+ {flowStep === 1 ? (
+
+ ⚡ {scenarioMeta.sourceText}
+
+ ) : (
+ 'Assigns riders internally, then streams status notifications onto the message bus.'
+ )}
+
+
+
+ {SOURCE_SUBJECTS.map((s) => (
+
+
+ {s}
+
+ ))}
+
+
+
+ {/* Conveyor 1 */}
+
+
+ {/* 2 · BUS STREAM */}
+
+
+
+ 2 · Message Bus
+
+
+
+
+
+
+ NATS JetStream
+
+
+
+ {flowStep === 2 ? (
+
+ ⚡ {scenarioMeta.busText}
+
+ ) : (
+ '6 durable event streams. Best-effort delivery — publish never blocks live dispatch.'
+ )}
+
+
+
+
+
+ Dropped 20 Sep (Stalled)
+
+
+
+
+ {/* Conveyor 2 */}
+
+
+ {/* 3 · AGENT SWARM */}
+
+
+
+ 3 · Intelligence Swarm
+
+
+ 9 Specialized Agents
+
+
+
+ {/* Real-time reasoning badge during Step 3 */}
+ {flowStep === 3 && (
+
+
+ {scenarioMeta.aiText}
+
+ )}
+
+
+ {/* Event-driven sub-cluster */}
+
+
+
+
+ Event-Driven Subscribers
+
+
+ Autonomous
+
+
+
+
+ {eventAgents.map((agent) => {
+ const isSelected = selectedAgentId === agent.id;
+ const isAgentActiveInFlow = flowStep === 3 && scenarioMeta.aiAgent === agent.id;
+
+ const statusChipTone = {
+ active: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800',
+ degraded: 'bg-amber-50 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800',
+ idle: 'bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-800/40 dark:text-slate-400 dark:border-slate-700',
+ }[agent.status];
+
+ return (
+
onSelectAgent(agent.id)}
+ className={cn(
+ 'flex flex-col gap-2 rounded-lg border p-2.5 text-left transition-all cursor-pointer relative bg-surface',
+ isAgentActiveInFlow
+ ? 'border-brand ring-2 ring-brand/30 shadow-xs'
+ : isSelected
+ ? 'border-brand ring-1 ring-brand/20'
+ : 'border-border hover:border-border-strong hover:shadow-2xs'
+ )}
+ >
+ {/* Top Row: Agent Name + High-tech Glowing Radar Beacon */}
+
+
+ {agent.name}
+
+
+
+
+
+
+ {/* Middle Row: Subscription Event Badge */}
+
+
+ {agent.subscribes[0] || 'Direct NATS'}
+
+
+
+ {/* Bottom Row: Event count & Status indicator */}
+
+ 0
+ ? 'text-emerald-700 dark:text-emerald-400 font-bold'
+ : 'text-ink-4'
+ )}
+ >
+ {agent.events > 0 ? `${agent.events} events` : '0 events'}
+
+
+ {agent.status}
+
+
+
+ );
+ })}
+
+
+
+ {/* Task-driven sub-cluster */}
+
+
+
+
+ Task-Driven Orchestration
+
+
+ JARVIS Delegated
+
+
+
+
+
onSelectAgent('JARVIS')}
+ className={cn(
+ 'flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-bold transition-all cursor-pointer bg-surface',
+ selectedAgentId === 'JARVIS'
+ ? 'border-brand ring-1 ring-brand/30 text-brand'
+ : 'border-border text-ink-2 hover:bg-surface-elevated'
+ )}
+ >
+
+ JARVIS (Master)
+
+
+
+ {taskAgents.map((agent) => {
+ const isAgentActiveInFlow = flowStep === 3 && scenarioMeta.aiAgent === agent.id;
+
+ return (
+ onSelectAgent(agent.id)}
+ className={cn(
+ 'rounded-md border px-2 py-1 text-[11px] font-medium transition-all cursor-pointer bg-surface',
+ isAgentActiveInFlow
+ ? 'border-brand ring-1 ring-brand/40 text-brand font-bold shadow-xs'
+ : selectedAgentId === agent.id
+ ? 'border-brand text-brand font-bold'
+ : 'border-border text-ink-2 hover:bg-surface-subtle'
+ )}
+ >
+ {agent.name}
+
+ );
+ })}
+
+
+
+
+
+
+ {/* Conveyor 3 */}
+
+
+ {/* 4 · OUTCOME */}
+
+
+
+ 4 · Safe Escalation
+
+
+
+
+ Alert Only (Never Act)
+
+
+ {flowStep === 4 ? (
+
+ ✓ {scenarioMeta.outcomeText}
+
+ ) : (
+ 'Emits ops desk alerts & customer delay SMS. Strictly limited to 1 escalation/zone/day.'
+ )}
+
+
+
+
+
+ {flowStep === 4 ? 'Flow Verified & Audited' : 'Autonomy Gates Locked'}
+
+
+
+
+
+ );
+}
+
+AgentPipeline.propTypes = {
+ selectedAgentId: PropTypes.string,
+ onSelectAgent: PropTypes.func.isRequired,
+ onFlowStepChange: PropTypes.func,
+};
diff --git a/src/pages/doormile/agents/AgentWorkflowView.jsx b/src/pages/doormile/agents/AgentWorkflowView.jsx
new file mode 100644
index 0000000..1eb26c2
--- /dev/null
+++ b/src/pages/doormile/agents/AgentWorkflowView.jsx
@@ -0,0 +1,312 @@
+import React, { useEffect, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
+import {
+ ArrowLeft,
+ ArrowRight,
+ CheckCircle2,
+ ChevronRight,
+ ExternalLink,
+ Layers,
+ Pause,
+ Play,
+ RotateCcw,
+ ShieldCheck,
+ Sparkles,
+ Zap,
+} from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { AGENT_WORKFLOWS } from './agentWorkflowsData';
+
+export default function AgentWorkflowView({
+ agentId,
+ onBackToNetwork,
+ onOpenTechnicalDrawer,
+}) {
+ const workflow = AGENT_WORKFLOWS[agentId] || AGENT_WORKFLOWS.ORDER_AGENT;
+ const reduce = useReducedMotion();
+
+ // Active step index in execution animation (0 to workflow.steps.length - 1)
+ const [activeStepIndex, setActiveStepIndex] = useState(0);
+ const [isPlaying, setIsPlaying] = useState(true);
+ const [selectedStepDetail, setSelectedStepDetail] = useState(null);
+
+ const steps = workflow.steps;
+ const timerRef = useRef(null);
+
+ // Animated execution loop: advances step by step
+ useEffect(() => {
+ if (!isPlaying) return;
+
+ timerRef.current = setTimeout(() => {
+ setActiveStepIndex((prev) => (prev + 1) % steps.length);
+ }, 1800);
+
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, [isPlaying, activeStepIndex, steps.length]);
+
+ const activeStep = steps[activeStepIndex];
+
+ return (
+
+ {/* ── Top Bar: Back Button & Agent Title ── */}
+
+
+
+
+ Back to Agent Network
+
+
+
+
+
+
+
+ {workflow.name}
+
+
+ — {workflow.subtitle}
+
+
+
+
+ {/* Right actions: Pause/Play & Technical Drawer */}
+
+
setIsPlaying(!isPlaying)}
+ className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-xs font-semibold text-ink-2 hover:bg-surface-elevated transition-colors cursor-pointer"
+ >
+ {isPlaying ? (
+ <>
+
+ Pause
+ >
+ ) : (
+ <>
+
+ Run
+ >
+ )}
+
+
+
onOpenTechnicalDrawer(agentId)}
+ className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-xs font-semibold text-ink-2 hover:border-brand/40 hover:text-brand transition-all cursor-pointer shadow-2xs"
+ >
+
+ Technical Specs
+
+
+
+
+ {/* ── Workflow Description Banner ── */}
+
+
+
+
+ {workflow.summary}
+
+
+
+
+ Autonomy Gated (0 DB writes)
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ THE WORKFLOW TIMELINE / PIPELINE (HERO VISUALIZATION)
+ ═══════════════════════════════════════════════════════════════════ */}
+
+
+
+
+ Execution Timeline
+
+
+ · Step {activeStepIndex + 1} of {steps.length}
+
+
+
+ Click any step to inspect details
+
+
+
+ {/* Responsive Pipeline Steps (Horizontal on Desktop, Vertical on Mobile) */}
+
+ {steps.map((stepItem, index) => {
+ const isCompleted = index < activeStepIndex;
+ const isProcessing = index === activeStepIndex;
+ const isWaiting = index > activeStepIndex;
+ const isSelected = selectedStepDetail?.id === stepItem.id;
+
+ return (
+
setSelectedStepDetail(stepItem)}
+ className={cn(
+ 'group relative flex flex-col gap-2 rounded-xl border p-3.5 transition-all duration-300 cursor-pointer bg-surface text-left',
+ isProcessing
+ ? 'border-brand ring-2 ring-brand/30 shadow-md scale-102 bg-surface-elevated z-10'
+ : isSelected
+ ? 'border-brand ring-1 ring-brand/40 bg-surface-elevated shadow-xs'
+ : isCompleted
+ ? 'border-border/80 hover:border-border hover:bg-surface-elevated'
+ : 'border-border/50 opacity-60 hover:opacity-90'
+ )}
+ >
+ {/* Step Index & State Indicator */}
+
+
+ 0{index + 1}
+
+
+ {/* Clean Visual State Beacon */}
+ {isCompleted ? (
+
+ ✓
+
+ ) : isProcessing ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+ {/* Step Title */}
+
+ {stepItem.title}
+
+
+ {/* Short Human-Readable Description (Max 2 lines) */}
+
+ {stepItem.description}
+
+
+ {/* Technical Output Tag */}
+
+
+ {stepItem.output}
+
+
+
+ );
+ })}
+
+
+ {/* ── Agent-to-Agent Transition Handover (Requirement: Show Where Work Goes Next) ── */}
+
+
+
+
+ Workflow Handover:
+
+
+ Upon completion, validated events transit directly to:
+
+
+
+
+
+ {workflow.nextAgent}
+
+
+
+
+
+
+ {/* ── Selected Step Detail Inspector ── */}
+
+ {selectedStepDetail && (
+
+
+
+
+
+ Step Detail: {selectedStepDetail.title}
+
+
+
setSelectedStepDetail(null)}
+ className="text-caption text-ink-4 hover:text-ink-1 transition-colors cursor-pointer"
+ >
+ Close ✕
+
+
+
+
+
+
+ What happens?
+
+
+ {selectedStepDetail.description}
+
+
+
+
+
+ Input Trigger
+
+
+ {selectedStepDetail.input}
+
+
+
+
+
+ Output Result
+
+
+ {selectedStepDetail.output}
+
+
+
+
+ )}
+
+
+ );
+}
+
+AgentWorkflowView.propTypes = {
+ agentId: PropTypes.string.isRequired,
+ onBackToNetwork: PropTypes.func.isRequired,
+ onOpenTechnicalDrawer: PropTypes.func.isRequired,
+};
diff --git a/src/pages/doormile/agents/Agents.jsx b/src/pages/doormile/agents/Agents.jsx
new file mode 100644
index 0000000..4201525
--- /dev/null
+++ b/src/pages/doormile/agents/Agents.jsx
@@ -0,0 +1,333 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { AnimatePresence, motion } from 'framer-motion';
+import {
+ Activity,
+ AlertTriangle,
+ ArrowUpRight,
+ Bot,
+ CheckCircle2,
+ ChevronRight,
+ ExternalLink,
+ Layers,
+ Radio,
+ RefreshCw,
+ Server,
+ ShieldAlert,
+ ShieldCheck,
+ Terminal,
+ Zap,
+} from 'lucide-react';
+
+import { PageHeader } from '@/components/ds';
+import { cn } from '@/lib/utils';
+import {
+ AGENTS,
+ LOG_ERROR_LINES,
+ LOG_TOTAL_LINES,
+ RUN_LOG,
+ agentById,
+ networkStats,
+} from '@/lib/agentNetwork';
+import AgentFlowVisualizer from './AgentFlowVisualizer';
+import AgentInspectorDrawer from './AgentInspectorDrawer';
+
+export default function Agents() {
+ const [selectedAgentId, setSelectedAgentId] = useState('DISPATCH_AGENT');
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+ const [activeLogModal, setActiveLogModal] = useState(null);
+
+ // Live Activity Stream (3–5 recent events, Requirement: LIVE ACTIVITY)
+ const [liveActivities, setLiveActivities] = useState([
+ {
+ id: 1,
+ time: '16:24:03',
+ agent: 'Dispatch',
+ action: 'Rider assignment completed',
+ color: '#10b981',
+ },
+ {
+ id: 2,
+ time: '16:24:01',
+ agent: 'Exception',
+ action: 'Rider inactivity detected',
+ color: '#f59e0b',
+ },
+ {
+ id: 3,
+ time: '16:23:58',
+ agent: 'JARVIS',
+ action: 'Assignment failure routed',
+ color: '#6366f1',
+ },
+ ]);
+
+ const handleOpenInspector = (agentId) => {
+ setSelectedAgentId(agentId);
+ setIsDrawerOpen(true);
+ };
+
+ const handleActivityLogged = (newActivity) => {
+ setLiveActivities((prev) => [
+ {
+ id: Date.now(),
+ time: newActivity.time,
+ agent: newActivity.agent,
+ action: newActivity.action,
+ color: newActivity.color,
+ },
+ ...prev.slice(0, 3), // Keep strictly 4 items max to avoid clutter
+ ]);
+ };
+
+ return (
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ COMPACT TOP STATUS BAR (REQUIREMENT: TOP STATUS BAR)
+ Clean, minimal telemetry ribbon: ● 1 Processing · ● 1 Warning · ● 7 Idle · 🔒 Autonomy locked
+ ═══════════════════════════════════════════════════════════════════ */}
+
+ {/* Left: Identity */}
+
+
+
+ Doormile AI Agent Network
+
+
+ · Real-Time Workflow Visualizer
+
+
+
+ {/* Right: Clean Status Indicators */}
+
+ {/* 1 Processing */}
+
+
+ 1 Processing
+
+
+ {/* 1 Warning */}
+
+
+ 1 Warning
+
+
+ {/* 7 Idle */}
+
+
+ 7 Idle
+
+
+ {/* 🔒 Autonomy Locked */}
+
+
+ Autonomy: 0 / 8
+
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ THE HERO: 4-STAGE AI FLOW VISUALIZER & DEDICATED WORKFLOWS
+ ═══════════════════════════════════════════════════════════════════ */}
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ SECONDARY SECTIONS: LIVE ACTIVITY & GROUPED SYSTEM DIAGNOSTICS
+ ═══════════════════════════════════════════════════════════════════ */}
+
+ {/* ── 1. Compact Live Activity Feed ── */}
+
+
+
+
+
+ {liveActivities.slice(0, 4).map((activity) => (
+
+
+
+
+ {activity.agent}
+
+ ·
+
+ {activity.action}
+
+
+
+
+ {activity.time}
+
+
+ ))}
+
+
+
+
+ {/* ── 2. Grouped System Diagnostics (Requirement: Group Repeated Errors) ── */}
+
+
+
+
+
+ System Diagnostics
+
+
+
+ Grouped health checks
+
+
+
+
+ {/* Grouped Error 1: NATS connection closed */}
+
+ setActiveLogModal({
+ title: 'NATS connection closed',
+ occurrences: '8,594 occurrences',
+ detail:
+ 'Both pull loops in ExceptionAgent repeatedly retried a closed NATS handle without backoff reconnection.',
+ solution:
+ 'Solution: Implemented auto-reconnect backoff on consumer handle.',
+ })
+ }
+ className="flex items-center justify-between rounded-lg border border-rose-200 dark:border-rose-900/60 bg-rose-50/50 dark:bg-rose-950/20 p-2.5 text-xs hover:border-rose-300 transition-colors cursor-pointer group"
+ >
+
+
+
+ NATS connection closed
+
+
+
+
+
+ 8,594 occurrences
+
+
+
+
+
+ {/* Grouped Error 2: Unknown zone */}
+
+ setActiveLogModal({
+ title: 'Unknown zone failure',
+ occurrences: '2 occurrences',
+ detail:
+ 'Go backend published booking.assignment_failed without a zone_id attribute. DispatchAgent automatically re-bucketed failures into H3 geo-cells.',
+ solution:
+ 'Solution: Added fallback geo-cell matching in dispatch pipeline.',
+ })
+ }
+ className="flex items-center justify-between rounded-lg border border-amber-200 dark:border-amber-900/60 bg-amber-50/50 dark:bg-amber-950/20 p-2.5 text-xs hover:border-amber-300 transition-colors cursor-pointer group"
+ >
+
+
+
+ Unknown zone
+
+
+
+
+
+ 2 occurrences
+
+
+
+
+
+
+
+
+ {/* ═══════════════════════════════════════════════════════════════════
+ DETAIL INSPECTOR DRAWER (TECHNICAL DETAILS LIVE HERE ONLY)
+ ═══════════════════════════════════════════════════════════════════ */}
+
setIsDrawerOpen(false)}
+ />
+
+ {/* ── Diagnostics Modal for Grouped Errors ── */}
+
+ {activeLogModal && (
+ <>
+ setActiveLogModal(null)}
+ className="fixed inset-0 z-50 bg-black/40 backdrop-blur-2xs"
+ />
+
+
+
+
+
+ {activeLogModal.title}
+
+
+
+ {activeLogModal.occurrences}
+
+
+
+
+
+ {activeLogModal.detail}
+
+
+
+ Remediation:
+
+ {activeLogModal.solution}
+
+
+
+
+ setActiveLogModal(null)}
+ className="rounded-lg border border-border bg-surface px-4 py-1.5 text-xs font-semibold text-ink-2 hover:bg-surface-elevated transition-colors cursor-pointer"
+ >
+ Close
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/pages/doormile/agents/CountUp.jsx b/src/pages/doormile/agents/CountUp.jsx
new file mode 100644
index 0000000..e4d5773
--- /dev/null
+++ b/src/pages/doormile/agents/CountUp.jsx
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { animate, useReducedMotion } from 'framer-motion';
+
+/**
+ * A number that counts up to its value.
+ *
+ * On a board where most figures are zero, watching one climb to 9 while its
+ * neighbour sits at 0 is the fastest way to see which of them is a real
+ * measurement — the motion itself carries the contrast.
+ *
+ * It settles on the exact target rather than wherever the last frame landed,
+ * because a KPI that reads 8 when the answer is 9 is worse than one that never
+ * animated. Under `prefers-reduced-motion` it renders the value immediately.
+ */
+export default function CountUp({ to, duration = 0.9 }) {
+ const reduce = useReducedMotion();
+ const [shown, setShown] = useState(reduce ? to : 0);
+
+ useEffect(() => {
+ if (reduce) {
+ setShown(to);
+ return undefined;
+ }
+ const controls = animate(0, to, {
+ duration,
+ ease: [0.22, 1, 0.36, 1],
+ onUpdate: (v) => setShown(Math.round(v)),
+ onComplete: () => setShown(to),
+ });
+ return () => controls.stop();
+ }, [to, duration, reduce]);
+
+ return <>{shown}>;
+}
+
+CountUp.propTypes = { to: PropTypes.number.isRequired, duration: PropTypes.number };
diff --git a/src/pages/doormile/agents/agentWorkflowsData.js b/src/pages/doormile/agents/agentWorkflowsData.js
new file mode 100644
index 0000000..85b7814
--- /dev/null
+++ b/src/pages/doormile/agents/agentWorkflowsData.js
@@ -0,0 +1,453 @@
+// ==============================|| 8 SPECIALIZED AGENT WORKFLOWS ||============================== //
+//
+// Tailored, human-readable step-by-step execution pipelines for each of the 8 agents.
+// JARVIS is the central orchestration layer and coordinates them.
+
+export const AGENT_WORKFLOWS = {
+ ORDER_AGENT: {
+ id: 'ORDER_AGENT',
+ name: 'Order Agent',
+ shortName: 'Order',
+ status: 'idle',
+ color: '#3b82f6', // blue
+ subtitle: 'How an order moves through the system',
+ summary: 'Validates, categorizes, and prepares customer orders for fulfillment.',
+ nextAgent: 'Dispatch Agent',
+ steps: [
+ {
+ id: 'create_order',
+ title: 'Create Order',
+ description: 'Customer places an order through the client app or API.',
+ input: 'customer.order_submitted',
+ output: 'order.raw_record',
+ },
+ {
+ id: 'validate_order',
+ title: 'Validate Order',
+ description: 'Check that the order contains all required delivery details and payment.',
+ input: 'order.raw_record',
+ output: 'order.validated',
+ },
+ {
+ id: 'check_customer',
+ title: 'Check Customer',
+ description: 'Verify customer delivery address, contact info, and special instructions.',
+ input: 'order.validated',
+ output: 'customer.verified',
+ },
+ {
+ id: 'categorize_order',
+ title: 'Categorize Order',
+ description: 'Determine order priority, package dimensions, and service level.',
+ input: 'customer.verified',
+ output: 'order.categorized',
+ },
+ {
+ id: 'publish_order',
+ title: 'Publish Order Event',
+ description: 'Broadcast order readiness to the internal event bus.',
+ input: 'order.categorized',
+ output: 'booking.created',
+ },
+ {
+ id: 'send_dispatch',
+ title: 'Send to Dispatch',
+ description: 'Pass the order to the dispatch workflow for rider assignment.',
+ input: 'booking.created',
+ output: 'handoff: DISPATCH_AGENT',
+ },
+ {
+ id: 'order_confirmed',
+ title: 'Order Confirmed',
+ description: 'The order is securely placed and ready for pickup execution.',
+ input: 'dispatch.acknowledged',
+ output: 'order.confirmed_active',
+ },
+ ],
+ },
+
+ DISPATCH_AGENT: {
+ id: 'DISPATCH_AGENT',
+ name: 'Dispatch Agent',
+ shortName: 'Dispatch',
+ status: 'active',
+ color: '#10b981', // green
+ subtitle: 'How rider matching and order dispatch works',
+ summary: 'Monitors rider assignment, checks geo-proximity, and allocates deliveries.',
+ nextAgent: 'Fleet / Customer',
+ steps: [
+ {
+ id: 'receive_order',
+ title: 'Receive Order',
+ description: 'Dispatch receives a new delivery request from the event bus.',
+ input: 'booking.assigned',
+ output: 'dispatch.received',
+ },
+ {
+ id: 'check_riders',
+ title: 'Check Available Riders',
+ description: 'Find active riders currently clocked in and ready for work.',
+ input: 'dispatch.received',
+ output: 'riders.pool_filtered',
+ },
+ {
+ id: 'check_zone',
+ title: 'Check Delivery Zone',
+ description: 'Check which riders are in the same 10km H3 geographic cell as the merchant.',
+ input: 'riders.pool_filtered',
+ output: 'riders.in_zone',
+ },
+ {
+ id: 'find_rider',
+ title: 'Find Suitable Rider',
+ description: 'Select the best available rider using capacity, rating, and distance.',
+ input: 'riders.in_zone',
+ output: 'rider.selected',
+ },
+ {
+ id: 'assign_rider',
+ title: 'Assign Rider',
+ description: 'Assign the delivery to the selected rider in the dispatch queue.',
+ input: 'rider.selected',
+ output: 'booking.assigned_miler',
+ },
+ {
+ id: 'confirm_dispatch',
+ title: 'Confirm Dispatch',
+ description: 'Confirm that the delivery has been successfully allocated.',
+ input: 'booking.assigned_miler',
+ output: 'dispatch.verified_active',
+ },
+ ],
+ },
+
+ EXCEPTION_AGENT: {
+ id: 'EXCEPTION_AGENT',
+ name: 'Exception Agent',
+ shortName: 'Exception',
+ status: 'degraded',
+ color: '#f59e0b', // amber
+ subtitle: 'How stall detection and operational issues are managed',
+ summary: 'Monitors rider GPS pings and booking progression to identify stalls.',
+ nextAgent: 'Operations Desk',
+ steps: [
+ {
+ id: 'detect_activity',
+ title: 'Detect Unusual Activity',
+ description: 'Exception loop spots a rider whose GPS location has stayed silent >120s.',
+ input: 'miler.location.updated',
+ output: 'anomaly.stalled_candidate',
+ },
+ {
+ id: 'check_severity',
+ title: 'Check Severity',
+ description: 'Assess the potential delay impact on the customer delivery promise.',
+ input: 'anomaly.stalled_candidate',
+ output: 'severity.calculated',
+ },
+ {
+ id: 'identify_order',
+ title: 'Identify Affected Order',
+ description: 'Isolate the specific active booking associated with the stalled rider.',
+ input: 'severity.calculated',
+ output: 'order.isolated',
+ },
+ {
+ id: 'determine_escalation',
+ title: 'Determine Escalation',
+ description: 'Check rate-limiter (1/zone/day) to prevent operations alert fatigue.',
+ input: 'order.isolated',
+ output: 'escalation.allowed',
+ },
+ {
+ id: 'notify_operations',
+ title: 'Notify Operations',
+ description: 'Surface a high-priority intervention card on the operations console.',
+ input: 'escalation.allowed',
+ output: 'ops.alert_dispatched',
+ },
+ {
+ id: 'track_resolution',
+ title: 'Track Resolution',
+ description: 'Monitor order until rider resumes movement or reassignment occurs.',
+ input: 'ops.alert_dispatched',
+ output: 'incident.resolved',
+ },
+ ],
+ },
+
+ EXPRESS_DISPATCH_AGENT: {
+ id: 'EXPRESS_DISPATCH_AGENT',
+ name: 'Express Dispatch',
+ shortName: 'Express Dispatch',
+ status: 'idle',
+ color: '#8b5cf6', // purple
+ subtitle: 'How priority and VIP delivery requests are handled',
+ summary: 'Provides expedited single-drop fulfillment with minimal staging latency.',
+ nextAgent: 'Dispatch Agent',
+ steps: [
+ {
+ id: 'receive_express',
+ title: 'Receive Express Request',
+ description: 'Immediate express dispatch request arrives from client booking path.',
+ input: 'express.dispatch_req',
+ output: 'express.ticket_open',
+ },
+ {
+ id: 'check_priority',
+ title: 'Check Priority',
+ description: 'Verify VIP SLA, guaranteed transit window, and express pricing.',
+ input: 'express.ticket_open',
+ output: 'sla.verified',
+ },
+ {
+ id: 'find_nearby',
+ title: 'Find Nearby Rider',
+ description: 'Query active riders within a tight 3km radius from pickup coordinate.',
+ input: 'sla.verified',
+ output: 'express_pool.ready',
+ },
+ {
+ id: 'check_availability',
+ title: 'Check Rider Availability',
+ description: 'Verify that rider has zero active overlapping jobs and empty cargo.',
+ input: 'express_pool.ready',
+ output: 'rider.single_task_ready',
+ },
+ {
+ id: 'assign_rider',
+ title: 'Assign Rider',
+ description: 'Reserve rider exclusively for direct point-to-point delivery.',
+ input: 'rider.single_task_ready',
+ output: 'express.allocated',
+ },
+ {
+ id: 'confirm_express',
+ title: 'Confirm Express Dispatch',
+ description: 'Confirm direct express dispatch and stream ETA to recipient.',
+ input: 'express.allocated',
+ output: 'express.confirmed_transit',
+ },
+ ],
+ },
+
+ ROUTE_OPTIMIZER: {
+ id: 'ROUTE_OPTIMIZER',
+ name: 'Route Optimizer',
+ shortName: 'Route Optimizer',
+ status: 'idle',
+ color: '#06b6d4', // cyan
+ subtitle: 'How multi-stop batch routes are solved and sequenced',
+ summary: 'Executes time-window vehicle routing optimization with a 10-minute cache.',
+ nextAgent: 'Fleet / Rider App',
+ steps: [
+ {
+ id: 'receive_batch',
+ title: 'Receive Delivery Orders',
+ description: 'Batch of orders accumulated at local hub ready for route compilation.',
+ input: 'hub.batch_ready',
+ output: 'batch.unsequenced',
+ },
+ {
+ id: 'group_zones',
+ title: 'Group Orders by Zone',
+ description: 'Cluster delivery waypoints by geographic neighborhood density.',
+ input: 'batch.unsequenced',
+ output: 'clusters.compiled',
+ },
+ {
+ id: 'check_riders',
+ title: 'Check Available Riders',
+ description: 'Match cluster cargo volume against available rider vehicle capacities.',
+ input: 'clusters.compiled',
+ output: 'riders.capacity_matched',
+ },
+ {
+ id: 'generate_routes',
+ title: 'Generate Route Options',
+ description: 'Run combinatorial solver for multi-stop pickup and drop sequences.',
+ input: 'riders.capacity_matched',
+ output: 'candidates.generated',
+ },
+ {
+ id: 'optimize_time',
+ title: 'Optimize Time and Distance',
+ description: 'Select route permutation that minimizes mileage and traffic delays.',
+ input: 'candidates.generated',
+ output: 'route.optimal_selected',
+ },
+ {
+ id: 'publish_route',
+ title: 'Publish Recommended Route',
+ description: 'Send sequenced waypoints directly to the rider mobile navigation app.',
+ input: 'route.optimal_selected',
+ output: 'route.published_cache',
+ },
+ ],
+ },
+
+ HUB_AGENT: {
+ id: 'HUB_AGENT',
+ name: 'Hub Agent',
+ shortName: 'Hub',
+ status: 'idle',
+ color: '#eab308', // yellow
+ subtitle: 'How sorting centers and transit storage operate',
+ summary: 'Tracks inventory at each hub and balances dock intake vs staging.',
+ nextAgent: 'Fleet Agent',
+ steps: [
+ {
+ id: 'receive_hub',
+ title: 'Receive Hub Event',
+ description: 'Package arrives at sorting facility for regional transit staging.',
+ input: 'hub.scan_arrival',
+ output: 'package.docked',
+ },
+ {
+ id: 'check_capacity',
+ title: 'Check Hub Capacity',
+ description: 'Assess available storage bays and sorting queue volume.',
+ input: 'package.docked',
+ output: 'capacity.safe',
+ },
+ {
+ id: 'check_incoming',
+ title: 'Check Incoming Deliveries',
+ description: 'Monitor inbound feeder vans arriving within the next 30 minutes.',
+ input: 'capacity.safe',
+ output: 'inbound.tracked',
+ },
+ {
+ id: 'check_outgoing',
+ title: 'Check Outgoing Deliveries',
+ description: 'Stage orders for outbound last-mile departure windows.',
+ input: 'inbound.tracked',
+ output: 'outbound.batched',
+ },
+ {
+ id: 'update_hub_state',
+ title: 'Update Hub State',
+ description: 'Update physical inventory counts in the hub ledger.',
+ input: 'outbound.batched',
+ output: 'hub.state_synced',
+ },
+ {
+ id: 'notify_fleet',
+ title: 'Notify Relevant Agent',
+ description: 'Alert fleet and dispatch agents that outbound pallets are staged.',
+ input: 'hub.state_synced',
+ output: 'handoff: FLEET_AGENT',
+ },
+ ],
+ },
+
+ FLEET_AGENT: {
+ id: 'FLEET_AGENT',
+ name: 'Fleet Agent',
+ shortName: 'Fleet',
+ status: 'idle',
+ color: '#ec4899', // pink
+ subtitle: 'How vehicles and rider assets are monitored',
+ summary: 'Maintains vehicle condition, battery state, and volumetric readiness.',
+ nextAgent: 'Dispatch Agent',
+ steps: [
+ {
+ id: 'receive_vehicle',
+ title: 'Receive Vehicle Update',
+ description: 'Telemetry beacon received from connected EV bike or delivery van.',
+ input: 'vehicle.telemetry_ping',
+ output: 'telemetry.logged',
+ },
+ {
+ id: 'check_status',
+ title: 'Check Vehicle Status',
+ description: 'Verify maintenance health, battery percentage, and inspection permits.',
+ input: 'telemetry.logged',
+ output: 'health.verified',
+ },
+ {
+ id: 'check_capacity',
+ title: 'Check Capacity',
+ description: 'Confirm available thermal box volume and payload rating.',
+ input: 'health.verified',
+ output: 'volume.available',
+ },
+ {
+ id: 'check_location',
+ title: 'Check Location',
+ description: 'Determine exact vehicle coordinates on the fleet live map.',
+ input: 'volume.available',
+ output: 'geo.resolved',
+ },
+ {
+ id: 'determine_availability',
+ title: 'Determine Availability',
+ description: 'Mark vehicle ready for high-load or multi-drop route allocation.',
+ input: 'geo.resolved',
+ output: 'asset.available_flag',
+ },
+ {
+ id: 'update_fleet_state',
+ title: 'Update Fleet State',
+ description: 'Sync updated vehicle availability with the dispatch routing engine.',
+ input: 'asset.available_flag',
+ output: 'fleet.pool_synced',
+ },
+ ],
+ },
+
+ CUSTOMER_AGENT: {
+ id: 'CUSTOMER_AGENT',
+ name: 'Customer Agent',
+ shortName: 'Customer',
+ status: 'idle',
+ color: '#14b8a6', // teal
+ subtitle: 'How delivery notifications and delay updates are sent',
+ summary: 'Composes live tracking SMS and delay notifications across channels.',
+ nextAgent: 'Customer App / SMS',
+ steps: [
+ {
+ id: 'receive_update',
+ title: 'Receive Delivery Update',
+ description: 'Order state changes (e.g. rider pickup, transit delay, arrival).',
+ input: 'order.milestone_reached',
+ output: 'milestone.evaluated',
+ },
+ {
+ id: 'understand_impact',
+ title: 'Understand Customer Impact',
+ description: 'Calculate real-time ETA change and determine if customer needs alerting.',
+ input: 'milestone.evaluated',
+ output: 'eta.calculated',
+ },
+ {
+ id: 'prepare_notification',
+ title: 'Prepare Notification',
+ description: 'Generate concise, reassuring message with live GPS tracking URL.',
+ input: 'eta.calculated',
+ output: 'message.drafted',
+ },
+ {
+ id: 'choose_channel',
+ title: 'Choose Communication',
+ description: 'Select optimal delivery channel: WhatsApp, Push, or SMS text.',
+ input: 'message.drafted',
+ output: 'channel.selected',
+ },
+ {
+ id: 'send_update',
+ title: 'Send Customer Update',
+ description: 'Transmit message payload through communications gateway.',
+ input: 'channel.selected',
+ output: 'gateway.dispatched',
+ },
+ {
+ id: 'track_notification',
+ title: 'Track Notification',
+ description: 'Verify message delivery receipt and customer interaction.',
+ input: 'gateway.dispatched',
+ output: 'notification.delivered',
+ },
+ ],
+ },
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index 0b4f8e2..8eeed2e 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -224,6 +224,31 @@ module.exports = {
'0%, 49%': { opacity: '1' },
'50%, 100%': { opacity: '0' },
},
+ /* Agent Network — every one of these is something the swarm does.
+
+ carry : a parcel moving down the belt from one pipeline stage to
+ the next, so the diagram reads as a path rather than boxes
+ radar : the 10km GEOSEARCH the dispatch agent runs to find a rider
+ stutter: a retry that never lands. Stepped, not eased, so it reads
+ as mechanical and stuck rather than alive and breathing
+ belt : a conveyor running under the one agent carrying load
+ grow-x : a load bar filling from zero, so it is a measurement */
+ carry: {
+ '0%': { transform: 'translateX(4px)', opacity: '0' },
+ '14%, 78%': { opacity: '1' },
+ '100%': { transform: 'translateX(34px)', opacity: '0' },
+ },
+ radar: {
+ '0%': { boxShadow: '0 0 0 0 rgb(21 128 61 / 0.55)' },
+ '70%': { boxShadow: '0 0 0 9px rgb(21 128 61 / 0)' },
+ '100%': { boxShadow: '0 0 0 0 rgb(21 128 61 / 0)' },
+ },
+ stutter: {
+ '0%, 100%': { boxShadow: '0 0 0 0 rgb(180 83 9 / 0.6)' },
+ '50%': { boxShadow: '0 0 0 4px rgb(180 83 9 / 0)' },
+ },
+ belt: { to: { backgroundPosition: '26px 0' } },
+ 'grow-x': { from: { transform: 'scaleX(0)' }, to: { transform: 'scaleX(1)' } },
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
@@ -236,6 +261,12 @@ module.exports = {
typing: 'typing 1.2s var(--ease-in-out) infinite',
'pulse-ring': 'pulse-ring 2s var(--ease-out) infinite',
caret: 'caret 1s steps(1) infinite',
+ carry: 'carry 2.3s cubic-bezier(.45,0,.55,1) infinite',
+ 'carry-slow': 'carry 3.1s cubic-bezier(.45,0,.55,1) .6s infinite',
+ radar: 'radar 2.4s var(--ease-out) infinite',
+ stutter: 'stutter 1.05s steps(3, end) infinite',
+ belt: 'belt 1s linear infinite',
+ 'grow-x': 'grow-x .7s var(--ease-out) .15s both',
},
},
},
diff --git a/tests/integration/agentsPage.test.jsx b/tests/integration/agentsPage.test.jsx
new file mode 100644
index 0000000..2111851
--- /dev/null
+++ b/tests/integration/agentsPage.test.jsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { render, screen, fireEvent, within } from '@testing-library/react';
+
+jest.mock('lucide-react', () =>
+ new Proxy(
+ {},
+ {
+ get: (_t, prop) =>
+ prop === '__esModule' ? true : (props) =>
,
+ }
+ )
+);
+
+import Agents from '@/pages/doormile/agents/Agents';
+
+/**
+ * The Agents page.
+ *
+ * It renders a hand-maintained table (lib/agentNetwork.js) rather than a live
+ * feed, so the risk here is not stale data — agentNetwork.test.js guards the
+ * table — it is that the page silently drops something. A detail panel that
+ * renders an empty string, or a card whose status has no colour, looks like a
+ * quiet page rather than a broken one.
+ */
+
+describe('the Agents page', () => {
+ it('lists all nine agents', () => {
+ render(
);
+ ['JARVIS', 'Dispatch', 'Exception', 'Express Dispatch', 'Order', 'Route Optimizer', 'Hub', 'Fleet', 'Customer'].forEach(
+ (name) => {
+ expect(screen.getAllByText(name).length).toBeGreaterThan(0);
+ }
+ );
+ });
+
+ it('opens on Dispatch rather than an empty panel', () => {
+ // It is the only agent with anything to show. Landing on a blank detail
+ // panel reads as a broken page.
+ render(
);
+ expect(screen.getByText('DISPATCH_AGENT')).toBeInTheDocument();
+ expect(screen.getByText(/only agent doing real work/i)).toBeInTheDocument();
+ });
+
+ it('swaps the whole detail panel when another agent is picked', () => {
+ render(
);
+ fireEvent.click(screen.getByRole('button', { name: /Exception/ }));
+
+ expect(screen.getByText('EXCEPTION_AGENT')).toBeInTheDocument();
+ expect(screen.getByText(/17,188 of the 18,146 lines/)).toBeInTheDocument();
+ // and the previous agent's record is gone, not merely scrolled away
+ expect(screen.queryByText('DISPATCH_AGENT')).not.toBeInTheDocument();
+ });
+
+ it('marks the selected card for a screen reader, not just with a colour', () => {
+ render(
);
+ const exception = screen.getByRole('button', { name: /Exception/ });
+ expect(exception).toHaveAttribute('aria-pressed', 'false');
+ fireEvent.click(exception);
+ expect(exception).toHaveAttribute('aria-pressed', 'true');
+ });
+
+ it('says a task-driven agent listens to nothing, rather than leaving a blank', () => {
+ render(
);
+ fireEvent.click(screen.getByRole('button', { name: /Fleet/ }));
+ expect(screen.getByText(/runs only when JARVIS hands it a task/i)).toBeInTheDocument();
+ });
+
+ it('shows the pipeline, both halves of it', () => {
+ render(
);
+ expect(screen.getByText('doormile_backend')).toBeInTheDocument();
+ expect(screen.getByText('NATS JetStream')).toBeInTheDocument();
+ expect(screen.getByText('Event-driven')).toBeInTheDocument();
+ expect(screen.getByText('Task-driven')).toBeInTheDocument();
+ expect(screen.getByText('Alert, never act')).toBeInTheDocument();
+ });
+
+ it('states that nothing is switched on, in the KPI row', () => {
+ // The page's central claim: the swarm observes and alerts, and does not
+ // write to the system.
+ render(
);
+ expect(screen.getByText(/Gates on/)).toBeInTheDocument();
+ expect(screen.getByText(/All autonomy gates OFF/)).toBeInTheDocument();
+ });
+
+ it('admits the figures are a snapshot, not a feed', () => {
+ // A status board that looks live and is four days old is worse than one
+ // that says how old it is.
+ render(
);
+ expect(screen.getAllByText(/Last run · 16–20 Sep 2026/).length).toBeGreaterThan(0);
+ });
+
+ it('shows the repeated-error count on the log rows', () => {
+ render(
);
+ const errs = screen.getAllByText('×8,594');
+ expect(errs).toHaveLength(2);
+ });
+
+ it('every card carries a real trigger label, not undefined', () => {
+ const { container } = render(
);
+ const cards = container.querySelectorAll('button[aria-pressed]');
+ expect(cards).toHaveLength(9);
+ cards.forEach((card) => {
+ const text = within(card).getByText(/NATS event|NATS pull|JARVIS task|In-process/);
+ expect(text).toBeInTheDocument();
+ });
+ });
+});
diff --git a/tests/lib/agentNetwork.test.js b/tests/lib/agentNetwork.test.js
new file mode 100644
index 0000000..5d02313
--- /dev/null
+++ b/tests/lib/agentNetwork.test.js
@@ -0,0 +1,153 @@
+import {
+ AGENTS,
+ LOG_ERROR_LINES,
+ LOG_TOTAL_LINES,
+ RUN_LOG,
+ TRIGGER,
+ agentById,
+ eventDrivenAgents,
+ networkStats,
+ taskDrivenAgents,
+} from '@/lib/agentNetwork';
+
+/**
+ * The agent table behind the Agents page.
+ *
+ * It is a hand-maintained snapshot, not a feed — AI_engine exposes no endpoint
+ * this console can reach. That makes these tests load-bearing in an unusual
+ * way: they are the only thing standing between a typo in the table and a
+ * status board that states something untrue with total confidence.
+ */
+
+describe('the swarm as recorded', () => {
+ it('is nine agents — JARVIS and eight specialised', () => {
+ expect(AGENTS).toHaveLength(9);
+ expect(AGENTS.map((a) => a.id)).toContain('JARVIS');
+ });
+
+ it('gives every agent a unique id', () => {
+ const ids = AGENTS.map((a) => a.id);
+ expect(new Set(ids).size).toBe(ids.length);
+ });
+
+ it('gives every agent a trigger the page can label', () => {
+ // A trigger with no entry in TRIGGER renders as `undefined` on the card —
+ // silently, because React prints nothing for it.
+ AGENTS.forEach((a) => {
+ expect(Object.keys(TRIGGER)).toContain(a.trigger);
+ });
+ });
+
+ it('gives every agent the fields the detail panel reads', () => {
+ AGENTS.forEach((a) => {
+ expect(typeof a.detail).toBe('string');
+ expect(a.detail.length).toBeGreaterThan(20);
+ expect(typeof a.publishes).toBe('string');
+ expect(typeof a.api).toBe('string');
+ expect(typeof a.autonomy).toBe('string');
+ expect(typeof a.lastSeen).toBe('string');
+ expect(Array.isArray(a.subscribes)).toBe(true);
+ });
+ });
+
+ it('uses only the four states the page can colour', () => {
+ // An unknown status falls through TONE to `undefined`, and the card then
+ // renders with no dot, no badge and no bar — invisible breakage.
+ AGENTS.forEach((a) => {
+ expect(['active', 'running', 'degraded', 'idle']).toContain(a.status);
+ });
+ });
+});
+
+describe('the split that the page is really about', () => {
+ it('three agents subscribe to the bus themselves', () => {
+ expect(eventDrivenAgents().map((a) => a.id)).toEqual([
+ 'DISPATCH_AGENT',
+ 'EXCEPTION_AGENT',
+ 'EXPRESS_DISPATCH_AGENT',
+ ]);
+ });
+
+ it('five only run when JARVIS hands them work', () => {
+ expect(taskDrivenAgents()).toHaveLength(5);
+ });
+
+ it('every event-driven agent names at least one subject', () => {
+ // The pipeline chip prints `subscribes[0]`. An empty array there renders a
+ // blank line where a NATS subject should be.
+ eventDrivenAgents().forEach((a) => {
+ expect(a.subscribes.length).toBeGreaterThan(0);
+ });
+ });
+
+ it('no task-driven agent claims to subscribe to anything', () => {
+ // The whole point of the second group: they cannot be woken by an event.
+ taskDrivenAgents().forEach((a) => {
+ expect(a.subscribes).toEqual([]);
+ });
+ });
+});
+
+describe('the headline numbers are derived, never written down', () => {
+ it('counts what the cards underneath actually show', () => {
+ // A dashboard that says "3 active" over three grey dots is the failure
+ // this guards. Every stat comes off the same table the grid renders.
+ const s = networkStats();
+ expect(s.total).toBe(AGENTS.length);
+ expect(s.working).toBe(AGENTS.filter((a) => a.status === 'active').length);
+ expect(s.degraded).toBe(AGENTS.filter((a) => a.status === 'degraded').length);
+ expect(s.working + s.degraded + s.idle + 1).toBe(s.total); // +1 for JARVIS, which is `running`
+ });
+
+ it('reports exactly one agent doing real work', () => {
+ expect(networkStats().working).toBe(1);
+ });
+
+ it('reports no autonomy gate switched on', () => {
+ // Stated on the page as a promise: the swarm observes and alerts, and does
+ // not write to the system. If a gate is ever turned on, this fails and the
+ // page's own subtitle has to change with it.
+ const s = networkStats();
+ expect(s.autonomyOn).toBe(0);
+ expect(s.gated).toBe(3);
+ });
+
+ it('recomputes for a different set rather than returning constants', () => {
+ const two = networkStats([
+ { status: 'active', events: 5, gated: true },
+ { status: 'idle', events: 0, gated: false },
+ ]);
+ expect(two.total).toBe(2);
+ expect(two.working).toBe(1);
+ expect(two.events).toBe(5);
+ expect(two.gated).toBe(1);
+ });
+});
+
+describe('agentById', () => {
+ it('finds an agent', () => {
+ expect(agentById('EXCEPTION_AGENT').name).toBe('Exception');
+ });
+
+ it('falls back to the first rather than returning undefined', () => {
+ // The page reads `.status` off the result straight away; undefined here is
+ // a blank panel at best and a crash at worst.
+ expect(agentById('NOPE')).toBe(AGENTS[0]);
+ });
+});
+
+describe('the run log', () => {
+ it('uses only tones the page can colour', () => {
+ RUN_LOG.forEach((l) => {
+ expect(['info', 'warn', 'error']).toContain(l.tone);
+ });
+ });
+
+ it("the two repeated errors account for the log's stated error total", () => {
+ // 2 x 8,594 = 17,188, which is the number in the panel header. If the
+ // table and the header ever disagree, one of them is lying.
+ const repeated = RUN_LOG.filter((l) => l.tone === 'error').reduce((n, l) => n + (l.count || 0), 0);
+ expect(repeated).toBe(LOG_ERROR_LINES);
+ expect(LOG_ERROR_LINES).toBeLessThan(LOG_TOTAL_LINES);
+ });
+});