document.addEventListener('alpine:init', () => { Alpine.data('agents', () => ({ agents: [], operations: [], loading: true, agentSearch: '', openDropdown: null, // drawer state drawerOpen: false, drawerMode: 'create', // 'create' | 'edit' editingId: null, // form fields form: { display_name: '', slug: '', description: '', status: 'active', selectedOps: [], }, // operations picker opSearch: '', slugManuallyEdited: false, async init() { const [agentsRes, opsRes] = await Promise.all([ fetch((window.DATA_URL||'data/')+'agents.json'), fetch((window.DATA_URL||'data/')+'operations.json'), ]); this.agents = await agentsRes.json(); this.operations = await opsRes.json(); this.loading = false; document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.drawerOpen) this.closeDrawer(); }); document.addEventListener('click', () => { this.openDropdown = null; }); }, // ── computed ── get filteredAgents() { const q = this.agentSearch.toLowerCase().trim(); if (!q) return this.agents; return this.agents.filter(a => a.display_name.toLowerCase().includes(q) || a.slug.toLowerCase().includes(q) || (a.description || '').toLowerCase().includes(q) ); }, get totalCalls() { return this.agents.reduce((s, a) => s + (a.calls_today || 0), 0); }, get activeCount() { return this.agents.filter(a => a.status === 'active').length; }, get totalOpsExposed() { return this.agents.reduce((s, a) => s + (a.operation_count || 0), 0); }, get filteredOps() { const q = this.opSearch.toLowerCase(); if (!q) return this.operations; return this.operations.filter(op => op.name.toLowerCase().includes(q) || op.display_name.toLowerCase().includes(q) ); }, // ── drawer ── openCreate() { this.drawerMode = 'create'; this.editingId = null; this.form = { display_name: '', slug: '', description: '', status: 'active', selectedOps: [] }; this.opSearch = ''; this.slugManuallyEdited = false; this.drawerOpen = true; }, openEdit(agent) { this.drawerMode = 'edit'; this.editingId = agent.id; this.form = { display_name: agent.display_name, slug: agent.slug, description: agent.description, status: agent.status, selectedOps: [...(agent.operation_ids || [])], }; this.opSearch = ''; this.slugManuallyEdited = true; this.drawerOpen = true; }, closeDrawer() { this.drawerOpen = false; }, // ── slug auto-derive ── onNameInput(val) { this.form.display_name = val; if (!this.slugManuallyEdited) { this.form.slug = val.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); } }, onSlugInput(val) { this.form.slug = val.toLowerCase().replace(/[^a-z0-9-]/g, ''); this.slugManuallyEdited = true; }, // ── operations picker ── toggleOp(opId) { const idx = this.form.selectedOps.indexOf(opId); if (idx === -1) this.form.selectedOps.push(opId); else this.form.selectedOps.splice(idx, 1); }, isOpSelected(opId) { return this.form.selectedOps.includes(opId); }, // ── save ── saveAgent() { if (!this.form.display_name.trim() || !this.form.slug.trim()) return; const ws = (localStorage.getItem('crank_workspace') || 'acme-workspace'); const endpoint = '/mcp/v1/' + ws + '/' + this.form.slug; if (this.drawerMode === 'create') { this.agents.unshift({ id: 'ag-' + Date.now(), slug: this.form.slug, display_name: this.form.display_name, description: this.form.description, status: this.form.status, operation_count: this.form.selectedOps.length, operation_ids: [...this.form.selectedOps], key_count: 0, created_at: new Date().toISOString(), created_by: 'AT', last_called_at: null, calls_today: 0, mcp_endpoint: endpoint, }); } else { const idx = this.agents.findIndex(a => a.id === this.editingId); if (idx !== -1) { this.agents[idx] = Object.assign({}, this.agents[idx], { display_name: this.form.display_name, slug: this.form.slug, description: this.form.description, status: this.form.status, operation_count: this.form.selectedOps.length, operation_ids: [...this.form.selectedOps], mcp_endpoint: endpoint, }); } } this.closeDrawer(); }, // ── delete ── deleteAgent(id) { this.agents = this.agents.filter(a => a.id !== id); }, // ── helpers ── mcpEndpoint(agent) { if (agent.mcp_endpoint) return agent.mcp_endpoint; const ws = localStorage.getItem('crank_workspace') || 'acme-workspace'; return '/mcp/v1/' + ws + '/' + agent.slug; }, copyEndpoint(agent) { navigator.clipboard.writeText(this.mcpEndpoint(agent)).catch(() => {}); }, statusClass(status) { return 'agent-status-badge agent-status-' + status; }, protocolBadge(op) { if (op.protocol === 'rest') return 'badge badge-rest'; if (op.protocol === 'graphql') return 'badge badge-graphql'; return 'badge badge-grpc'; }, protocolLabel(op) { if (op.protocol === 'rest') return 'REST'; if (op.protocol === 'graphql') return 'GQL'; return 'gRPC'; }, formatDate(dateStr) { if (!dateStr) return '—'; return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }, formatCalls(n) { if (!n) return '0'; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }, })); });