{"app":{"id":30,"summary":"Enterprise Tool Orchestration Dashboard","versions":[8869],"created_by":"tristan795","created_at":"2026-03-31T12:32:46.498Z","votes":0,"approved":false,"apps":[],"app_type":"raw","external_embed_url":"https://app.windmill.dev/public/windmill-labs/7f87bf37aa5e19f3e6cfcd663ec1573f","value":{"runnables":{"get_tools":{"name":"get_tools","type":"inline","fields":{},"inlineScript":{"lock":"{\n  \"dependencies\": {}\n}\n//bun.lock\n<empty>","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","language":"bun"}}},"files":{"/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              &#x26A0; 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\">&#x26A0;</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","/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","/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","/package.json":"{\n    \"dependencies\": {\n        \"react\": \"19.0.0\",\n        \"react-dom\": \"19.0.0\",\n        \"windmill-client\": \"^1\"\n    },\n    \"devDependencies\": {\n        \"@types/react-dom\": \"^19.0.0\",\n        \"@types/react\": \"^19.0.0\"\n    }\n}\n"},"data":{"tables":[]}},"description":"Enterprise-wide visibility into internal tools, ownership, and usage across business units (mocked data)","vcreated_at":"2026-03-31T12:32:46.498Z","vcreated_by":"tristan795","comments":[]}}