Enterprise Tool Orchestration Dashboard
Enterprise-wide visibility into internal tools, ownership, and usage across business units (mocked data)
| 1 | { |
| 2 | "summary": "Enterprise Tool Orchestration Dashboard", |
| 3 | "value": { |
| 4 | "runnables": { |
| 5 | "get_tools": { |
| 6 | "name": "get_tools", |
| 7 | "type": "inline", |
| 8 | "fields": {}, |
| 9 | "inlineScript": { |
| 10 | "lock": "{\n \"dependencies\": {}\n}\n//bun.lock\n<empty>", |
| 11 | "content": "export async function main() {\n return [\n { id: 1, name: 'Customer Health API', type: 'API', creator: 'Sarah Chen', owningBU: 'Sales', createdDate: '2025-11-12', status: 'active', description: 'Scores customer health based on usage patterns', category: 'Customer Scoring', executionsPerWeek: 420, activeUsers: 18, lastUsed: '2026-03-30' },\n { id: 2, name: 'Account Risk Scorer', type: 'Script', creator: 'James Okonkwo', owningBU: 'Finance', createdDate: '2026-01-08', status: 'active', description: 'Evaluates financial risk for customer accounts', category: 'Customer Scoring', executionsPerWeek: 185, activeUsers: 7, lastUsed: '2026-03-29' },\n { id: 3, name: 'Client Retention Model', type: 'Bot', creator: 'Emma Larsson', owningBU: 'Marketing', createdDate: '2026-02-15', status: 'active', description: 'Predicts churn probability from engagement data', category: 'Customer Scoring', executionsPerWeek: 310, activeUsers: 12, lastUsed: '2026-03-31' },\n { id: 4, name: 'Revenue Forecast Dashboard', type: 'Dashboard', creator: 'James Okonkwo', owningBU: 'Finance', createdDate: '2025-10-20', status: 'active', description: 'Quarterly revenue projections and trends', category: 'Revenue Reporting', executionsPerWeek: 95, activeUsers: 34, lastUsed: '2026-03-31' },\n { id: 5, name: 'Sales Pipeline Tracker', type: 'Dashboard', creator: 'Marcus Rivera', owningBU: 'Sales', createdDate: '2025-12-03', status: 'active', description: 'Real-time pipeline value and conversion rates', category: 'Revenue Reporting', executionsPerWeek: 280, activeUsers: 22, lastUsed: '2026-03-31' },\n { id: 6, name: 'Lead Enrichment API', type: 'API', creator: 'Priya Patel', owningBU: 'Marketing', createdDate: '2025-11-28', status: 'active', description: 'Enriches lead data from external sources', category: 'Lead Management', executionsPerWeek: 650, activeUsers: 15, lastUsed: '2026-03-30' },\n { id: 7, name: 'Prospect Scoring Bot', type: 'Bot', creator: 'Sarah Chen', owningBU: 'Sales', createdDate: '2026-01-22', status: 'active', description: 'Ranks inbound prospects by conversion likelihood', category: 'Lead Management', executionsPerWeek: 390, activeUsers: 11, lastUsed: '2026-03-31' },\n { id: 8, name: 'ETL Pipeline Runner', type: 'Workflow', creator: 'Lin Zhang', owningBU: 'Engineering', createdDate: '2025-10-05', status: 'active', description: 'Orchestrates nightly data warehouse loads', category: 'Data Sync', executionsPerWeek: 56, activeUsers: 4, lastUsed: '2026-03-31' },\n { id: 9, name: 'Slack Notification Bot', type: 'Bot', creator: 'Lin Zhang', owningBU: 'Engineering', createdDate: '2025-11-01', status: 'active', description: 'Routes alerts to team channels based on severity', category: 'Notifications', executionsPerWeek: 780, activeUsers: 45, lastUsed: '2026-03-31' },\n { id: 10, name: 'Invoice Generator', type: 'Script', creator: 'James Okonkwo', owningBU: 'Finance', createdDate: '2025-10-15', status: 'active', description: 'Generates and sends PDF invoices to clients', category: 'Billing', executionsPerWeek: 120, activeUsers: 8, lastUsed: '2026-03-28' },\n { id: 11, name: 'PTO Request Workflow', type: 'Workflow', creator: 'Ana Kowalski', owningBU: 'HR', createdDate: '2026-01-10', status: 'active', description: 'Handles time-off requests with manager approval', category: 'Employee Self-Service', executionsPerWeek: 65, activeUsers: 38, lastUsed: '2026-03-31' },\n { id: 12, name: 'Candidate Screening Bot', type: 'Bot', creator: 'Ana Kowalski', owningBU: 'HR', createdDate: '2026-02-01', status: 'active', description: 'Pre-screens resumes against job requirements', category: 'Recruiting', executionsPerWeek: 210, activeUsers: 6, lastUsed: '2026-03-29' },\n { id: 13, name: 'Campaign Performance Tracker', type: 'Dashboard', creator: 'Priya Patel', owningBU: 'Marketing', createdDate: '2025-12-18', status: 'active', description: 'Tracks ROI and engagement across campaigns', category: 'Campaign Analytics', executionsPerWeek: 140, activeUsers: 19, lastUsed: '2026-03-31' },\n { id: 14, name: 'Vendor Onboarding Flow', type: 'Workflow', creator: 'David Park', owningBU: 'Operations', createdDate: '2026-01-15', status: 'active', description: 'Automates vendor registration and compliance checks', category: 'Procurement', executionsPerWeek: 35, activeUsers: 5, lastUsed: '2026-03-27' },\n { id: 15, name: 'Inventory Alert System', type: 'Script', creator: 'David Park', owningBU: 'Operations', createdDate: '2025-11-22', status: 'active', description: 'Monitors stock levels and triggers reorder alerts', category: 'Inventory', executionsPerWeek: 340, activeUsers: 9, lastUsed: '2026-03-31' },\n { id: 16, name: 'Expense Report Automator', type: 'Workflow', creator: 'James Okonkwo', owningBU: 'Finance', createdDate: '2025-12-10', status: 'active', description: 'Automates expense categorization and approval', category: 'Expense Management', executionsPerWeek: 88, activeUsers: 27, lastUsed: '2026-03-30' },\n { id: 17, name: 'Spend Tracker App', type: 'App', creator: 'David Park', owningBU: 'Operations', createdDate: '2026-02-20', status: 'active', description: 'Team-level spending dashboard with budget alerts', category: 'Expense Management', executionsPerWeek: 55, activeUsers: 14, lastUsed: '2026-03-31' },\n { id: 18, name: 'CI/CD Monitor', type: 'Dashboard', creator: 'Lin Zhang', owningBU: 'Engineering', createdDate: '2025-10-28', status: 'active', description: 'Build status, deploy frequency, and failure rates', category: 'DevOps', executionsPerWeek: 160, activeUsers: 12, lastUsed: '2026-03-31' },\n { id: 19, name: 'Quarterly OKR Dashboard', type: 'Dashboard', creator: 'Ana Kowalski', owningBU: 'HR', createdDate: '2026-03-15', status: 'draft', description: 'Tracks company-wide OKR progress', category: 'Performance', executionsPerWeek: 3, activeUsers: 1, lastUsed: '2026-03-25' },\n { id: 20, name: 'Legacy CRM Sync', type: 'Script', creator: 'Marcus Rivera', owningBU: 'Sales', createdDate: '2025-10-01', status: 'deprecated', description: 'Synced data between old CRM and warehouse', category: 'Data Sync', executionsPerWeek: 2, activeUsers: 0, lastUsed: '2026-01-15' },\n { id: 21, name: 'Old Billing Reconciler', type: 'Script', creator: 'James Okonkwo', owningBU: 'Finance', createdDate: '2025-10-08', status: 'deprecated', description: 'Legacy billing reconciliation script', category: 'Billing', executionsPerWeek: 0, activeUsers: 0, lastUsed: '2025-12-20' },\n { id: 22, name: 'Onboarding Checklist App', type: 'App', creator: 'Ana Kowalski', owningBU: 'HR', createdDate: '2026-01-25', status: 'active', description: 'Interactive onboarding tasks for new hires', category: 'Employee Onboarding', executionsPerWeek: 45, activeUsers: 16, lastUsed: '2026-03-28' },\n { id: 23, name: 'SEO Rank Tracker', type: 'Script', creator: 'Emma Larsson', owningBU: 'Marketing', createdDate: '2026-03-10', status: 'draft', description: 'Monitors keyword rankings across search engines', category: 'SEO', executionsPerWeek: 5, activeUsers: 1, lastUsed: '2026-03-20' },\n { id: 24, name: 'Warehouse Capacity Planner', type: 'App', creator: 'David Park', owningBU: 'Operations', createdDate: '2026-02-05', status: 'active', description: 'Visualizes warehouse utilization and forecasts', category: 'Capacity Planning', executionsPerWeek: 70, activeUsers: 6, lastUsed: '2026-03-31' },\n ]\n}\n", |
| 12 | "language": "bun" |
| 13 | } |
| 14 | } |
| 15 | }, |
| 16 | "files": { |
| 17 | "/App.tsx": "import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'\nimport { backend } from './wmill'\nimport './index.css'\n\ntype ToolType = 'API' | 'Script' | 'Dashboard' | 'Bot' | 'Workflow' | 'App'\ntype ToolStatus = 'active' | 'deprecated' | 'draft'\ntype BusinessUnit = 'Engineering' | 'Sales' | 'Finance' | 'Marketing' | 'Operations' | 'HR'\n\ntype Tool = {\n id: number\n name: string\n type: ToolType\n creator: string\n owningBU: BusinessUnit\n createdDate: string\n status: ToolStatus\n description: string\n category: string\n executionsPerWeek: number\n activeUsers: number\n lastUsed: string\n}\n\nconst STATUS_CONFIG: Record<ToolStatus, { color: string; bg: string; label: string }> = {\n active: { color: '#0d9668', bg: '#ecfdf5', label: 'Active' },\n deprecated: { color: '#e5484d', bg: '#fef2f2', label: 'Deprecated' },\n draft: { color: '#8b8fa3', bg: '#f3f4f6', label: 'Draft' },\n}\n\nconst TYPE_CONFIG: Record<ToolType, { color: string; bg: string }> = {\n API: { color: '#6c63ff', bg: '#f0efff' },\n Script: { color: '#0ea5e9', bg: '#ecfeff' },\n Dashboard: { color: '#f59e0b', bg: '#fffbeb' },\n Bot: { color: '#10b981', bg: '#ecfdf5' },\n Workflow: { color: '#8b5cf6', bg: '#f5f3ff' },\n App: { color: '#ec4899', bg: '#fdf2f8' },\n}\n\nconst BU_COLORS: Record<BusinessUnit, string> = {\n Engineering: '#6c63ff',\n Sales: '#f59e0b',\n Finance: '#10b981',\n Marketing: '#ec4899',\n Operations: '#0ea5e9',\n HR: '#8b5cf6',\n}\n\nconst BUSINESS_UNITS: BusinessUnit[] = ['Engineering', 'Sales', 'Finance', 'Marketing', 'Operations', 'HR']\nconst TOOL_TYPES: ToolType[] = ['API', 'Script', 'Dashboard', 'Bot', 'Workflow', 'App']\n\n\n\ntype GraphNode = {\n id: number\n x: number\n y: number\n vx: number\n vy: number\n tool: Tool\n}\n\ntype GraphEdge = {\n source: number\n target: number\n category: string\n}\n\nfunction BrainView({ tools }: { tools: Tool[] }) {\n const svgRef = useRef<SVGSVGElement>(null)\n const nodesRef = useRef<GraphNode[]>([])\n const animRef = useRef<number>(0)\n const [positions, setPositions] = useState<{ x: number; y: number }[]>([])\n const [hoveredNode, setHoveredNode] = useState<number | null>(null)\n const [dimensions, setDimensions] = useState({ width: 900, height: 560 })\n\n const edges = useMemo(() => {\n const result: GraphEdge[] = []\n for (let i = 0; i < tools.length; i++) {\n for (let j = i + 1; j < tools.length; j++) {\n if (tools[i].category === tools[j].category) {\n result.push({ source: tools[i].id, target: tools[j].id, category: tools[i].category })\n }\n }\n }\n return result\n }, [tools])\n\n const isDuplicate = useMemo(() => {\n const dupCategories = new Set<string>()\n const byCategory: Record<string, Set<string>> = {}\n tools.forEach(t => {\n if (t.status === 'deprecated') return\n if (!byCategory[t.category]) byCategory[t.category] = new Set()\n byCategory[t.category].add(t.owningBU)\n })\n Object.entries(byCategory).forEach(([cat, bus]) => {\n if (bus.size > 1) dupCategories.add(cat)\n })\n return dupCategories\n }, [tools])\n\n useEffect(() => {\n if (tools.length === 0) return\n\n const cx = dimensions.width / 2\n const cy = dimensions.height / 2\n const buAngles: Record<string, number> = {}\n BUSINESS_UNITS.forEach((bu, i) => {\n buAngles[bu] = (i / BUSINESS_UNITS.length) * Math.PI * 2\n })\n\n const nodes: GraphNode[] = tools.map((tool) => {\n const angle = buAngles[tool.owningBU] + (Math.random() - 0.5) * 0.8\n const radius = 120 + Math.random() * 100\n return {\n id: tool.id,\n x: cx + Math.cos(angle) * radius,\n y: cy + Math.sin(angle) * radius,\n vx: 0,\n vy: 0,\n tool,\n }\n })\n\n nodesRef.current = nodes\n // Show initial positions immediately\n setPositions(nodes.map(n => ({ x: n.x, y: n.y })))\n\n let tick = 0\n const maxTicks = 300\n const nodeMap = new Map(nodes.map(n => [n.id, n]))\n\n function simulateTick() {\n const alpha = Math.max(0.001, 1 - tick / maxTicks)\n\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i + 1; j < nodes.length; j++) {\n const dx = nodes[j].x - nodes[i].x\n const dy = nodes[j].y - nodes[i].y\n const dist = Math.sqrt(dx * dx + dy * dy) || 1\n const force = (800 * alpha) / (dist * dist)\n const fx = (dx / dist) * force\n const fy = (dy / dist) * force\n nodes[i].vx -= fx\n nodes[i].vy -= fy\n nodes[j].vx += fx\n nodes[j].vy += fy\n }\n }\n\n edges.forEach(edge => {\n const a = nodeMap.get(edge.source)\n const b = nodeMap.get(edge.target)\n if (!a || !b) return\n const dx = b.x - a.x\n const dy = b.y - a.y\n const dist = Math.sqrt(dx * dx + dy * dy) || 1\n const force = dist * 0.01 * alpha\n const fx = (dx / dist) * force\n const fy = (dy / dist) * force\n a.vx += fx\n a.vy += fy\n b.vx -= fx\n b.vy -= fy\n })\n\n nodes.forEach(n => {\n const angle = buAngles[n.tool.owningBU]\n const targetX = cx + Math.cos(angle) * 160\n const targetY = cy + Math.sin(angle) * 160\n n.vx += (targetX - n.x) * 0.005 * alpha\n n.vy += (targetY - n.y) * 0.005 * alpha\n })\n\n nodes.forEach(n => {\n n.vx += (cx - n.x) * 0.001 * alpha\n n.vy += (cy - n.y) * 0.001 * alpha\n })\n\n nodes.forEach(n => {\n n.vx *= 0.6\n n.vy *= 0.6\n n.x += n.vx\n n.y += n.vy\n n.x = Math.max(40, Math.min(dimensions.width - 40, n.x))\n n.y = Math.max(40, Math.min(dimensions.height - 40, n.y))\n })\n\n tick++\n setPositions(nodes.map(n => ({ x: n.x, y: n.y })))\n\n if (tick < maxTicks) {\n animRef.current = requestAnimationFrame(simulateTick)\n }\n }\n\n animRef.current = requestAnimationFrame(simulateTick)\n return () => cancelAnimationFrame(animRef.current)\n }, [tools, edges, dimensions])\n\n useEffect(() => {\n function handleResize() {\n if (svgRef.current) {\n const rect = svgRef.current.parentElement?.getBoundingClientRect()\n if (rect) setDimensions({ width: rect.width, height: 560 })\n }\n }\n handleResize()\n window.addEventListener('resize', handleResize)\n return () => window.removeEventListener('resize', handleResize)\n }, [])\n\n const nodeMap = useMemo(() => {\n const map = new Map<number, { x: number; y: number; tool: Tool }>()\n tools.forEach((tool, i) => {\n if (positions[i]) {\n map.set(tool.id, { ...positions[i], tool })\n }\n })\n return map\n }, [tools, positions])\n\n const hoveredTool = hoveredNode !== null ? tools.find(t => t.id === hoveredNode) : null\n const hoveredEdges = hoveredNode !== null\n ? edges.filter(e => e.source === hoveredNode || e.target === hoveredNode)\n : []\n const connectedIds = new Set(hoveredEdges.flatMap(e => [e.source, e.target]))\n\n return (\n <div className=\"brain-container\">\n <svg ref={svgRef} width={dimensions.width} height={dimensions.height} className=\"brain-svg\">\n {/* BU labels */}\n {BUSINESS_UNITS.map((bu, i) => {\n const angle = (i / BUSINESS_UNITS.length) * Math.PI * 2\n const lx = dimensions.width / 2 + Math.cos(angle) * (Math.min(dimensions.width, dimensions.height) / 2 - 30)\n const ly = dimensions.height / 2 + Math.sin(angle) * (dimensions.height / 2 - 30)\n return (\n <text\n key={bu}\n x={lx}\n y={ly}\n textAnchor=\"middle\"\n dominantBaseline=\"middle\"\n fill={BU_COLORS[bu]}\n fontSize=\"11\"\n fontWeight=\"600\"\n opacity={0.6}\n >\n {bu}\n </text>\n )\n })}\n\n {/* Edges */}\n {edges.map((edge, i) => {\n const a = nodeMap.get(edge.source)\n const b = nodeMap.get(edge.target)\n if (!a || !b) return null\n const isDup = isDuplicate.has(edge.category)\n const isHighlighted = hoveredNode !== null && (edge.source === hoveredNode || edge.target === hoveredNode)\n const dimmed = hoveredNode !== null && !isHighlighted\n return (\n <line\n key={i}\n x1={a.x}\n y1={a.y}\n x2={b.x}\n y2={b.y}\n stroke={isDup ? '#f59e0b' : '#d1d5db'}\n strokeWidth={isDup ? (isHighlighted ? 2.5 : 1.5) : (isHighlighted ? 2 : 0.8)}\n opacity={dimmed ? 0.08 : isDup ? 0.5 : 0.25}\n strokeDasharray={isDup ? '6 3' : 'none'}\n />\n )\n })}\n\n {/* Nodes */}\n {tools.map((tool, i) => {\n const pos = positions[i]\n if (!pos) return null\n const buColor = BU_COLORS[tool.owningBU]\n const r = Math.max(6, Math.min(14, 4 + tool.activeUsers / 5))\n const isHovered = hoveredNode === tool.id\n const isConnected = connectedIds.has(tool.id)\n const dimmed = hoveredNode !== null && !isHovered && !isConnected\n return (\n <g key={tool.id} onMouseEnter={() => setHoveredNode(tool.id)} onMouseLeave={() => setHoveredNode(null)}>\n <circle\n cx={pos.x}\n cy={pos.y}\n r={isHovered ? r + 3 : r}\n fill={buColor}\n opacity={dimmed ? 0.15 : tool.status === 'deprecated' ? 0.35 : 0.85}\n stroke={isHovered ? '#1a1a2e' : isDuplicate.has(tool.category) ? '#f59e0b' : 'white'}\n strokeWidth={isHovered ? 2 : isDuplicate.has(tool.category) ? 1.5 : 1}\n style={{ cursor: 'pointer', transition: 'r 0.15s, opacity 0.15s' }}\n />\n {isHovered && (\n <text\n x={pos.x}\n y={pos.y - r - 8}\n textAnchor=\"middle\"\n fill=\"#1a1a2e\"\n fontSize=\"12\"\n fontWeight=\"600\"\n >\n {tool.name}\n </text>\n )}\n </g>\n )\n })}\n </svg>\n\n {/* Tooltip */}\n {hoveredTool && (\n <div className=\"brain-tooltip\">\n <div style={{ fontWeight: 600, marginBottom: 4 }}>{hoveredTool.name}</div>\n <div style={{ fontSize: 12, color: '#8b8fa3', marginBottom: 6 }}>{hoveredTool.description}</div>\n <div style={{ display: 'flex', gap: 12, fontSize: 12 }}>\n <span className=\"badge\" style={{ color: TYPE_CONFIG[hoveredTool.type].color, background: TYPE_CONFIG[hoveredTool.type].bg }}>{hoveredTool.type}</span>\n <span style={{ color: BU_COLORS[hoveredTool.owningBU], fontWeight: 500 }}>{hoveredTool.owningBU}</span>\n <span style={{ color: '#8b8fa3' }}>{hoveredTool.activeUsers} users</span>\n </div>\n {isDuplicate.has(hoveredTool.category) && (\n <div style={{ marginTop: 6, fontSize: 11, color: '#f59e0b', fontWeight: 500 }}>\n ⚠ Duplication: {hoveredTool.category}\n </div>\n )}\n </div>\n )}\n\n {/* Legend */}\n <div className=\"brain-legend\">\n {BUSINESS_UNITS.map(bu => (\n <div key={bu} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>\n <span style={{ width: 8, height: 8, borderRadius: '50%', background: BU_COLORS[bu], flexShrink: 0 }} />\n <span style={{ fontSize: 11, color: '#8b8fa3' }}>{bu}</span>\n </div>\n ))}\n <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4 }}>\n <span style={{ width: 16, height: 0, borderTop: '2px dashed #f59e0b', flexShrink: 0 }} />\n <span style={{ fontSize: 11, color: '#f59e0b' }}>Duplication</span>\n </div>\n </div>\n </div>\n )\n}\n\nexport default function App() {\n const [tools, setTools] = useState<Tool[]>([])\n const [loading, setLoading] = useState(true)\n const [searchQuery, setSearchQuery] = useState('')\n const [filterBU, setFilterBU] = useState<'all' | BusinessUnit>('all')\n const [filterType, setFilterType] = useState<'all' | ToolType>('all')\n const [view, setView] = useState<'list' | 'brain'>('brain')\n\n useEffect(() => {\n async function loadData() {\n try {\n const data = await backend.get_tools()\n setTools(Array.isArray(data) ? data as Tool[] : [])\n } catch (e) {\n console.error('Error loading tools:', e)\n }\n setLoading(false)\n }\n loadData()\n }, [])\n\n const filteredTools = useMemo(() => {\n return tools.filter(tool => {\n const q = searchQuery.toLowerCase()\n const matchesSearch = !q ||\n tool.name.toLowerCase().includes(q) ||\n tool.description.toLowerCase().includes(q) ||\n tool.creator.toLowerCase().includes(q) ||\n tool.category.toLowerCase().includes(q)\n const matchesBU = filterBU === 'all' || tool.owningBU === filterBU\n const matchesType = filterType === 'all' || tool.type === filterType\n return matchesSearch && matchesBU && matchesType\n })\n }, [tools, searchQuery, filterBU, filterType])\n\n const kpis = useMemo(() => ({\n total: tools.length,\n active: tools.filter(t => t.status === 'active').length,\n bus: new Set(tools.map(t => t.owningBU)).size,\n }), [tools])\n\n const duplicationAlerts = useMemo(() => {\n const byCategory: Record<string, Tool[]> = {}\n tools.forEach(t => {\n if (t.status === 'deprecated') return\n if (!byCategory[t.category]) byCategory[t.category] = []\n byCategory[t.category].push(t)\n })\n return Object.entries(byCategory)\n .filter(([, tools]) => {\n const uniqueBUs = new Set(tools.map(t => t.owningBU))\n return uniqueBUs.size > 1\n })\n .map(([category, tools]) => ({ category, tools }))\n }, [tools])\n\n const buBreakdown = useMemo(() => {\n return BUSINESS_UNITS.map(bu => ({\n bu,\n count: tools.filter(t => t.owningBU === bu).length,\n }))\n }, [tools])\n\n const maxBUCount = Math.max(...buBreakdown.map(b => b.count))\n\n if (loading) {\n return (\n <div className=\"dashboard\" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '60vh' }}>\n <div style={{ color: '#8b8fa3', fontSize: 14 }}>Loading tools...</div>\n </div>\n )\n }\n\n return (\n <div className=\"dashboard\">\n <div className=\"header\">\n <h1 className=\"header-title\">Tool Orchestration</h1>\n <p className=\"header-subtitle\">Enterprise-wide visibility into internal tools, ownership, and usage across business units</p>\n </div>\n\n <div className=\"filter-bar\">\n <div className=\"search-box\">\n <svg className=\"search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n </svg>\n <input\n className=\"search-input\"\n type=\"text\"\n placeholder=\"Search tools, creators, categories...\"\n value={searchQuery}\n onChange={e => setSearchQuery(e.target.value)}\n />\n </div>\n <select\n className=\"filter-select\"\n value={filterBU}\n onChange={e => setFilterBU(e.target.value as 'all' | BusinessUnit)}\n >\n <option value=\"all\">All Business Units</option>\n {BUSINESS_UNITS.map(bu => <option key={bu} value={bu}>{bu}</option>)}\n </select>\n <select\n className=\"filter-select\"\n value={filterType}\n onChange={e => setFilterType(e.target.value as 'all' | ToolType)}\n >\n <option value=\"all\">All Types</option>\n {TOOL_TYPES.map(t => <option key={t} value={t}>{t}</option>)}\n </select>\n <div className=\"view-toggle\">\n <button className={`view-btn ${view === 'brain' ? 'active' : ''}`} onClick={() => setView('brain')}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\"><circle cx=\"4\" cy=\"4\" r=\"2\"/><circle cx=\"12\" cy=\"4\" r=\"2\"/><circle cx=\"8\" cy=\"12\" r=\"2\"/><line x1=\"5.5\" y1=\"5.5\" x2=\"7\" y2=\"10.5\"/><line x1=\"10.5\" y1=\"5.5\" x2=\"9\" y2=\"10.5\"/><line x1=\"6\" y1=\"4\" x2=\"10\" y2=\"4\"/></svg>\n Brain\n </button>\n <button className={`view-btn ${view === 'list' ? 'active' : ''}`} onClick={() => setView('list')}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\"><line x1=\"3\" y1=\"4\" x2=\"13\" y2=\"4\"/><line x1=\"3\" y1=\"8\" x2=\"13\" y2=\"8\"/><line x1=\"3\" y1=\"12\" x2=\"13\" y2=\"12\"/></svg>\n List\n </button>\n </div>\n </div>\n\n <div className=\"kpi-grid\">\n <div className=\"kpi-card\">\n <div className=\"kpi-label\">Total Tools</div>\n <div className=\"kpi-value\">{kpis.total}</div>\n </div>\n <div className=\"kpi-card\">\n <div className=\"kpi-label\">Active</div>\n <div className=\"kpi-value\">{kpis.active}</div>\n </div>\n <div className=\"kpi-card\">\n <div className=\"kpi-label\">Business Units</div>\n <div className=\"kpi-value\">{kpis.bus}</div>\n </div>\n <div className=\"kpi-card\">\n <div className=\"kpi-label\">Duplication Alerts</div>\n <div className=\"kpi-value alert\">{duplicationAlerts.length}</div>\n </div>\n </div>\n\n {view === 'brain' ? (\n <BrainView tools={filteredTools} />\n ) : (\n <div className=\"content-grid\">\n <div className=\"table-section\">\n <div className=\"table-header\">\n <div>\n <span className=\"table-title\">Tool Inventory</span>\n <span className=\"table-count\">{filteredTools.length} tools</span>\n </div>\n </div>\n <div className=\"table-container\">\n <table className=\"data-table\">\n <thead>\n <tr>\n <th>Tool</th>\n <th>Type</th>\n <th>Owner</th>\n <th>Status</th>\n <th>Exec / week</th>\n <th>Users</th>\n </tr>\n </thead>\n <tbody>\n {filteredTools.map(tool => {\n const typeStyle = TYPE_CONFIG[tool.type]\n const statusStyle = STATUS_CONFIG[tool.status]\n return (\n <tr key={tool.id}>\n <td>\n <div className=\"tool-name\">{tool.name}</div>\n <div className=\"tool-creator\">{tool.creator}</div>\n </td>\n <td>\n <span className=\"badge\" style={{ color: typeStyle.color, background: typeStyle.bg }}>\n {tool.type}\n </span>\n </td>\n <td>\n <span style={{ color: BU_COLORS[tool.owningBU], fontWeight: 500 }}>\n {tool.owningBU}\n </span>\n </td>\n <td>\n <span className=\"badge\" style={{ color: statusStyle.color, background: statusStyle.bg }}>\n <span className=\"status-dot\" style={{ background: statusStyle.color }} />\n {statusStyle.label}\n </span>\n </td>\n <td className=\"stat-value\">{tool.executionsPerWeek.toLocaleString()}</td>\n <td className=\"stat-muted\">{tool.activeUsers}</td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n </div>\n\n <div className=\"sidebar\">\n <div className=\"sidebar-card\">\n <div className=\"sidebar-title\">Tools by Business Unit</div>\n {buBreakdown.map(({ bu, count }) => (\n <div className=\"bu-row\" key={bu}>\n <span className=\"bu-dot\" style={{ background: BU_COLORS[bu] }} />\n <span className=\"bu-name\">{bu}</span>\n <div className=\"bu-bar-track\">\n <div className=\"bu-bar-fill\" style={{ width: `${(count / maxBUCount) * 100}%`, background: BU_COLORS[bu] }} />\n </div>\n <span className=\"bu-count\">{count}</span>\n </div>\n ))}\n </div>\n\n <div className=\"sidebar-card\">\n <div className=\"sidebar-title\">Duplication Alerts</div>\n {duplicationAlerts.map(alert => (\n <div className=\"dup-alert\" key={alert.category}>\n <div className=\"dup-header\">\n <span className=\"dup-icon\">⚠</span>\n <span className=\"dup-category\">{alert.category}</span>\n <span className=\"dup-count\">{alert.tools.length} tools</span>\n </div>\n <div className=\"dup-tools\">\n {alert.tools.map(t => (\n <div className=\"dup-tool\" key={t.id}>\n <span>{t.name}</span>\n <span className=\"dup-bu\" style={{ color: BU_COLORS[t.owningBU] }}>{t.owningBU}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n </div>\n )\n}\n", |
| 18 | "/index.css": "* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;\n background: #fafbfc;\n color: #1a1a2e;\n font-size: 14px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n#root {\n width: 100%;\n min-height: 100vh;\n}\n\n.dashboard {\n max-width: 1400px;\n margin: 0 auto;\n padding: 32px 48px 64px;\n}\n\n/* Header */\n.header {\n margin-bottom: 28px;\n}\n\n.header-title {\n font-size: 26px;\n font-weight: 700;\n letter-spacing: -0.5px;\n color: #1a1a2e;\n margin-bottom: 4px;\n}\n\n.header-subtitle {\n font-size: 14px;\n color: #8b8fa3;\n font-weight: 400;\n}\n\n/* Filter Bar */\n.filter-bar {\n display: flex;\n gap: 12px;\n margin-bottom: 24px;\n align-items: center;\n}\n\n.search-input {\n padding: 9px 16px 9px 38px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n font-size: 13px;\n font-family: inherit;\n width: 280px;\n outline: none;\n transition: all 0.2s ease;\n background: #ffffff;\n color: #1a1a2e;\n}\n\n.search-input::placeholder {\n color: #a0a4b8;\n}\n\n.search-input:focus {\n border-color: #6c63ff;\n box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1);\n}\n\n.search-box {\n position: relative;\n}\n\n.search-icon {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n width: 16px;\n height: 16px;\n opacity: 0.35;\n}\n\n.filter-select {\n padding: 9px 32px 9px 14px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n font-size: 13px;\n font-family: inherit;\n outline: none;\n background: #ffffff;\n color: #1a1a2e;\n cursor: pointer;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238b8fa3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n transition: all 0.2s ease;\n}\n\n.filter-select:focus {\n border-color: #6c63ff;\n box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1);\n}\n\n/* KPI Cards */\n.kpi-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 20px;\n margin-bottom: 28px;\n}\n\n.kpi-card {\n background: #ffffff;\n border: 1px solid rgba(0, 0, 0, 0.06);\n border-radius: 14px;\n padding: 22px 24px;\n transition: all 0.25s ease;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n\n.kpi-card:hover {\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);\n transform: translateY(-2px);\n}\n\n.kpi-label {\n font-size: 12px;\n color: #8b8fa3;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n margin-bottom: 8px;\n}\n\n.kpi-value {\n font-size: 34px;\n font-weight: 700;\n color: #1a1a2e;\n letter-spacing: -1.5px;\n line-height: 1;\n font-variant-numeric: tabular-nums;\n}\n\n.kpi-value.alert {\n color: #e5484d;\n}\n\n/* Content Grid */\n.content-grid {\n display: grid;\n grid-template-columns: 2fr 1fr;\n gap: 24px;\n align-items: start;\n}\n\n/* Table Section */\n.table-section {\n background: #ffffff;\n border: 1px solid rgba(0, 0, 0, 0.06);\n border-radius: 14px;\n overflow: hidden;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n\n.table-header {\n padding: 20px 24px;\n border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.table-title {\n font-size: 16px;\n font-weight: 600;\n color: #1a1a2e;\n letter-spacing: -0.3px;\n}\n\n.table-count {\n font-size: 13px;\n color: #8b8fa3;\n font-weight: 400;\n margin-left: 8px;\n}\n\n.table-container {\n overflow-x: auto;\n}\n\n.data-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.data-table thead {\n background: #f8f9fa;\n border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n}\n\n.data-table th {\n padding: 12px 20px;\n text-align: left;\n font-size: 11px;\n font-weight: 600;\n color: #8b8fa3;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n white-space: nowrap;\n}\n\n.data-table td {\n padding: 14px 20px;\n border-bottom: 1px solid rgba(0, 0, 0, 0.04);\n font-size: 14px;\n color: #1a1a2e;\n}\n\n.data-table tbody tr {\n transition: background 0.15s ease;\n}\n\n.data-table tbody tr:hover {\n background: #f8f9fb;\n}\n\n.data-table tbody tr:last-child td {\n border-bottom: none;\n}\n\n.tool-name {\n font-weight: 600;\n color: #1a1a2e;\n}\n\n.tool-creator {\n font-size: 12px;\n color: #8b8fa3;\n margin-top: 2px;\n}\n\n.badge {\n display: inline-flex;\n align-items: center;\n padding: 3px 10px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.status-dot {\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n margin-right: 6px;\n flex-shrink: 0;\n}\n\n.stat-value {\n font-variant-numeric: tabular-nums;\n color: #1a1a2e;\n}\n\n.stat-muted {\n font-variant-numeric: tabular-nums;\n color: #8b8fa3;\n}\n\n/* Sidebar */\n.sidebar {\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n\n.sidebar-card {\n background: #ffffff;\n border: 1px solid rgba(0, 0, 0, 0.06);\n border-radius: 14px;\n padding: 24px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);\n}\n\n.sidebar-title {\n font-size: 13px;\n font-weight: 600;\n color: #8b8fa3;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n margin-bottom: 20px;\n}\n\n/* BU Breakdown */\n.bu-row {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 14px;\n}\n\n.bu-row:last-child {\n margin-bottom: 0;\n}\n\n.bu-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.bu-name {\n font-size: 13px;\n font-weight: 500;\n color: #1a1a2e;\n width: 100px;\n flex-shrink: 0;\n}\n\n.bu-bar-track {\n flex: 1;\n height: 8px;\n background: #f3f4f6;\n border-radius: 4px;\n overflow: hidden;\n}\n\n.bu-bar-fill {\n height: 100%;\n border-radius: 4px;\n transition: width 0.4s ease;\n}\n\n.bu-count {\n font-size: 13px;\n font-weight: 600;\n color: #1a1a2e;\n width: 20px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* Duplication Alerts */\n.dup-alert {\n border-left: 3px solid #f59e0b;\n background: #fffbeb;\n border-radius: 0 10px 10px 0;\n padding: 14px 16px;\n margin-bottom: 12px;\n}\n\n.dup-alert:last-child {\n margin-bottom: 0;\n}\n\n.dup-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n}\n\n.dup-icon {\n font-size: 14px;\n line-height: 1;\n}\n\n.dup-category {\n font-size: 14px;\n font-weight: 600;\n color: #1a1a2e;\n}\n\n.dup-count {\n font-size: 12px;\n color: #8b8fa3;\n margin-left: auto;\n}\n\n.dup-tool {\n display: flex;\n justify-content: space-between;\n align-items: center;\n font-size: 13px;\n color: #4a4a5e;\n padding: 3px 0;\n}\n\n.dup-bu {\n font-size: 12px;\n font-weight: 500;\n}\n\n/* View Toggle */\n.view-toggle {\n display: flex;\n gap: 0;\n margin-left: auto;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 10px;\n overflow: hidden;\n}\n\n.view-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 14px;\n font-size: 13px;\n font-family: inherit;\n font-weight: 500;\n color: #8b8fa3;\n background: #ffffff;\n border: none;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.view-btn:first-child {\n border-right: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.view-btn.active {\n color: #1a1a2e;\n background: #f3f4f6;\n}\n\n.view-btn:hover:not(.active) {\n background: #f8f9fa;\n}\n\n/* Brain View */\n.brain-container {\n position: relative;\n background: #ffffff;\n border: 1px solid rgba(0, 0, 0, 0.06);\n border-radius: 14px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);\n overflow: hidden;\n}\n\n.brain-svg {\n display: block;\n width: 100%;\n}\n\n.brain-tooltip {\n position: absolute;\n top: 20px;\n left: 20px;\n background: #ffffff;\n border: 1px solid rgba(0, 0, 0, 0.08);\n border-radius: 12px;\n padding: 14px 18px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);\n max-width: 280px;\n pointer-events: none;\n font-size: 13px;\n}\n\n.brain-legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(8px);\n border: 1px solid rgba(0, 0, 0, 0.06);\n border-radius: 10px;\n padding: 12px 16px;\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n\n/* Responsive */\n@media (max-width: 1200px) {\n .content-grid {\n grid-template-columns: 1fr;\n }\n}\n\n@media (max-width: 900px) {\n .kpi-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n\n .dashboard {\n padding: 24px;\n }\n\n .filter-bar {\n flex-wrap: wrap;\n }\n\n .search-input {\n width: 100%;\n }\n}\n", |
| 19 | "/index.tsx": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst root = createRoot(document.getElementById('root')!);\nroot.render(<App />);\n", |
| 20 | "/package.json": "{\n \"dependencies\": {\n \"react\": \"19.0.0\",\n \"react-dom\": \"19.0.0\",\n \"@orvanta/client\": \"^1\"\n },\n \"devDependencies\": {\n \"@types/react-dom\": \"^19.0.0\",\n \"@types/react\": \"^19.0.0\"\n }\n}\n" |
| 21 | }, |
| 22 | "data": { |
| 23 | "tables": [] |
| 24 | } |
| 25 | } |
| 26 | } |