// AGENTFORGE-IA — Integraciones, Ajustes, Facturación, Inicio, Ejecuciones, Analítica // --------------------------------------------------------------------------- // Modal de conexión / configuración de integración // --------------------------------------------------------------------------- const IntegrationModal = ({ integration, onClose, onSaved }) => { const [apiKey, setApiKey] = React.useState(''); const [accountName, setAccountName] = React.useState(integration.account_name || ''); const [saving, setSaving] = React.useState(false); const [testing, setTesting] = React.useState(false); const [err, setErr] = React.useState(''); const [testResult, setTestResult] = React.useState(null); const [copied, setCopied] = React.useState(false); const token = () => sessionStorage.getItem('af_token') || ''; const isWebhook = integration.kind === 'webhook'; const isOauth = integration.kind === 'oauth'; const isEdit = integration.connected; const canConnect = integration.can_connect !== false; const handleSave = async () => { if (!canConnect) { setErr(integration.blocked_reason || 'No tienes permisos para esta integración.'); return; } if (!isWebhook && !isOauth && !apiKey && !isEdit) { setErr('Introduce la API key.'); return; } setSaving(true); setErr(''); try { const body = {}; if (!isWebhook && !isOauth && apiKey) body.api_key = apiKey; if (accountName) body.account_name = accountName; const res = await fetch(`/api/v1/integrations/${integration.slug}/connect`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token() }, body: JSON.stringify(body), }); if (!res.ok) { const d = await res.json().catch(() => ({})); setErr(d.detail || `Error ${res.status}`); return; } const saved = await res.json(); onSaved(saved); onClose(); } catch (_) { setErr('Error de red. Inténtalo de nuevo.'); } finally { setSaving(false); } }; const handleOauthConnect = async () => { if (!canConnect) { setErr(integration.blocked_reason || 'No tienes permisos para esta integración.'); return; } setSaving(true); setErr(''); try { const res = await fetch(`/api/v1/integrations/${integration.slug}/oauth/start`, { method: 'POST', headers: { Authorization: 'Bearer ' + token() }, }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.auth_url) { setErr(data.detail || 'No se pudo iniciar OAuth.'); return; } window.location.href = data.auth_url; } catch (_) { setErr('Error de red al iniciar OAuth.'); } finally { setSaving(false); } }; const handleTest = async () => { if (!canConnect) { setTestResult({ ok: false, message: integration.blocked_reason || 'No tienes permisos para probar esta integración.' }); return; } setTesting(true); setTestResult(null); try { const res = await fetch(`/api/v1/integrations/${integration.slug}/test`, { method: 'POST', headers: { Authorization: 'Bearer ' + token() }, }); const d = await res.json().catch(() => ({ ok: false, message: 'Error de red.' })); setTestResult(d); } catch (_) { setTestResult({ ok: false, message: 'Error de red.' }); } finally { setTesting(false); } }; const copyWebhook = () => { navigator.clipboard.writeText(integration.webhook_url || '').then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Header */}
{integration.label}
{isEdit ? 'Configurar' : 'Conectar'} {integration.name}
{isWebhook ? 'Integración por webhook' : 'Integración por API key'}
{integration.scope === 'global_admin' && ( Global/Admin )}
{integration.blocked_reason && (
{integration.blocked_reason}
)} {/* Webhook kind — solo mostrar URL para copiar */} {isWebhook ? (

Usa esta URL en {integration.name} para enviar eventos a AgentForge:

{integration.webhook_url}
) : isOauth ? (

Esta integración se conecta por OAuth seguro.

Cuenta sugerida: adminforge@adminforge-ia.pro

) : ( /* API key kind */
{ setApiKey(e.target.value); setErr(''); }} /> {isEdit && integration.api_key_masked && (
Actual: {integration.api_key_masked}
)}
setAccountName(e.target.value)} />
)} {/* Último test */} {isEdit && integration.last_tested_at && (
{integration.last_test_ok ? 'Último test OK' : 'Último test fallido'} {new Date(integration.last_tested_at).toLocaleString('es-ES', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}
)} {/* Resultado de test en tiempo real */} {testResult && (
{testResult.ok ? 'Conexion OK: ' : 'Error: '}{testResult.message}
)} {err && (
{err}
)} {/* Acciones */}
{isEdit && !isWebhook && !isOauth && ( )} {isOauth && ( )} {!isWebhook && !isOauth && ( )}
); }; // --------------------------------------------------------------------------- // Modal de confirmación de desconexión // --------------------------------------------------------------------------- const DisconnectConfirmModal = ({ integration, onClose, onConfirm }) => { const [loading, setLoading] = React.useState(false); const handleConfirm = async () => { setLoading(true); await onConfirm(); setLoading(false); }; return (
{ if (e.target === e.currentTarget) onClose(); }} >
Desconectar {integration.name}

Se eliminarán las credenciales guardadas. Los agentes que usen esta integración dejarán de tener acceso. Esta acción no se puede deshacer.

); }; // --------------------------------------------------------------------------- // Catálogo de integraciones disponibles (enriquecido con desc, reqs y deployTime) // --------------------------------------------------------------------------- const INTEGRATIONS_CATALOG = [ { slug: 'salesforce', name: 'Salesforce', icon: 'salesforce', desc: 'Sincroniza leads, contactos y oportunidades con tu CRM Salesforce en tiempo real.', reqs: ['API key Salesforce', 'Perfil con permisos API'], deployTime: '~30 min' }, { slug: 'hubspot', name: 'HubSpot', icon: 'hubspot', desc: 'Conecta contactos, pipelines y workflows de HubSpot con tus agentes IA.', reqs: ['Cuenta HubSpot con API access', 'Token privado de app'], deployTime: '~20 min' }, { slug: 'slack', name: 'Slack', icon: 'slack', desc: 'Envía notificaciones y recibe comandos desde canales de Slack.', reqs: ['Workspace Slack admin', 'App Slack configurada'], deployTime: '~15 min' }, { slug: 'whatsapp', name: 'WhatsApp', icon: 'whatsapp', desc: 'Canal de atención vía WhatsApp Business API con respuestas automáticas.', reqs: ['Número WhatsApp Business verificado', 'Meta Business Manager'], deployTime: '~2 h' }, { slug: 'gmail', name: 'Gmail', icon: 'gmail', desc: 'Procesa emails entrantes y envía respuestas automáticas desde Gmail.', reqs: ['Cuenta Google Workspace', 'OAuth2 habilitado'], deployTime: '~20 min' }, { slug: 'notion', name: 'Notion', icon: 'notion', desc: 'Lee y escribe en bases de datos Notion para gestión de conocimiento.', reqs: ['Integration token Notion', 'Permisos de página'], deployTime: '~15 min' }, { slug: 'zapier', name: 'Zapier', icon: 'zapier', desc: 'Conecta con 5.000+ apps a través de Zaps automatizados.', reqs: ['Cuenta Zapier (plan Team o superior)'], deployTime: '~10 min' }, { slug: 'stripe', name: 'Stripe', icon: 'stripe', desc: 'Gestiona pagos, suscripciones y eventos de facturación.', reqs: ['API keys Stripe', 'Webhook configurado'], deployTime: '~25 min' }, { slug: 'mailchimp', name: 'Mailchimp', icon: 'mailchimp', desc: 'Sincroniza listas de contactos y automatiza campañas de email marketing.', reqs: ['API key Mailchimp', 'Audience ID'], deployTime: '~15 min' }, { slug: 'make', name: 'Make', icon: 'make', desc: 'Orquesta flujos complejos entre múltiples apps con Make (ex-Integromat).', reqs: ['Cuenta Make', 'Webhook de entrada'], deployTime: '~20 min' }, { slug: 'pipedrive', name: 'Pipedrive', icon: 'pipedrive', desc: 'Actualiza deals y actividades en tu pipeline de ventas Pipedrive.', reqs: ['API token Pipedrive'], deployTime: '~20 min' }, { slug: 'telegram', name: 'Telegram', icon: 'telegram', desc: 'Bot de Telegram para soporte y notificaciones automatizadas.', reqs: ['Bot token de @BotFather'], deployTime: '~15 min' }, ]; // Enriquece un objeto de integración del API con los metadatos del catálogo const enrichIntegration = (i) => { const meta = INTEGRATIONS_CATALOG.find(c => c.slug === i.slug) || {}; return { ...meta, ...i }; }; // --------------------------------------------------------------------------- // Metadatos de redes sociales OAuth personal // --------------------------------------------------------------------------- const SOCIAL_PROVIDERS = { instagram: { name: 'Instagram', color: '#e1306c', label: 'IG', desc: 'Conecta tu cuenta de Instagram para que el agente gestione mensajes y publicaciones.' }, facebook: { name: 'Facebook', color: '#1877f2', label: 'FB', desc: 'Conecta tu cuenta de Facebook para responder mensajes y publicar contenido.' }, tiktok: { name: 'TikTok', color: '#010101', label: 'TT', desc: 'Conecta tu cuenta de TikTok para gestionar comentarios y mensajes directos.' }, }; // --------------------------------------------------------------------------- // Componente principal Integrations // --------------------------------------------------------------------------- const Integrations = () => { const [ints, setInts] = React.useState([]); const [loading, setLoading] = React.useState(true); const [modal, setModal] = React.useState(null); // { integration } | null const [confirmDisconnect, setConfirmDisconnect] = React.useState(null); // integration | null const [err, setErr] = React.useState(''); // Social OAuth state const [socialStatus, setSocialStatus] = React.useState([]); const [socialLoading, setSocialLoading] = React.useState(false); const [socialConnecting, setSocialConnecting] = React.useState(null); const token = () => sessionStorage.getItem('af_token') || ''; // Carga el estado real de integraciones desde el API const loadIntegrations = React.useCallback(async () => { setLoading(true); setErr(''); try { const res = await fetch('/api/v1/integrations', { headers: { Authorization: 'Bearer ' + token() }, }); if (res.ok) { const raw = await res.json(); setInts(raw.map(enrichIntegration)); } else { setErr('No se pudieron cargar las integraciones.'); } } catch (_) { setErr('Error de red al cargar integraciones.'); } setLoading(false); }, []); React.useEffect(() => { loadIntegrations(); }, [loadIntegrations]); const connected = ints.filter(i => i.connected); const available = ints.filter(i => !i.connected); const handleSaved = (updated) => { const next = enrichIntegration(updated); setInts(prev => { const exists = prev.some(i => i.slug === next.slug); if (exists) return prev.map(i => i.slug === next.slug ? next : i); return [...prev, next]; }); }; const handleDisconnect = async (integration) => { try { const res = await fetch(`/api/v1/integrations/${integration.slug}`, { method: 'DELETE', headers: { Authorization: 'Bearer ' + token() }, }); if (res.ok || res.status === 204) { setInts(prev => prev.map(i => i.slug === integration.slug ? { ...i, connected: false, status: null, account_name: null, api_key_masked: null, last_tested_at: null, last_test_ok: null } : i )); setConfirmDisconnect(null); } } catch (_) {} }; // Formatea la fecha del último test const fmtTested = (iso) => { if (!iso) return null; const d = new Date(iso); return d.toLocaleString('es-ES', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }); }; // Carga el estado de conexiones sociales del usuario const loadSocialStatus = React.useCallback(async () => { setSocialLoading(true); try { const res = await fetch('/api/v1/social/status', { headers: { Authorization: 'Bearer ' + token() }, }); if (res.ok) setSocialStatus(await res.json()); } catch (_) {} setSocialLoading(false); }, []); React.useEffect(() => { loadSocialStatus(); }, [loadSocialStatus]); // Inicia el flujo OAuth de una red social en popup const handleSocialConnect = async (provider) => { const tk = token(); if (!tk) return; setSocialConnecting(provider); try { const res = await fetch(`/api/v1/social/connect/${provider}`, { headers: { Authorization: 'Bearer ' + tk }, }); if (!res.ok) { setSocialConnecting(null); return; } const data = await res.json(); const popup = window.open( data.auth_url, `social_oauth_${provider}`, 'width=620,height=720,scrollbars=yes,resizable=yes' ); if (!popup) { notify('Permite ventanas emergentes para conectar la red social'); setSocialConnecting(null); return; } const interval = setInterval(async () => { try { if (popup.closed) { clearInterval(interval); setSocialConnecting(null); return; } // Throws cross-origin error while popup is on provider domain — expected const popupUrl = new URL(popup.location.href); if (popupUrl.origin !== window.location.origin) return; const code = popupUrl.searchParams.get('code'); const state = popupUrl.searchParams.get('state'); if (code && state) { clearInterval(interval); popup.close(); const xres = await fetch( `/api/v1/social/exchange/${provider}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`, { method: 'POST', headers: { Authorization: 'Bearer ' + tk } } ); if (xres.ok) { const d = await xres.json(); const provName = SOCIAL_PROVIDERS[provider]?.name || provider; notify(`${provName} conectado${d.username ? ': @' + d.username : ''}`); loadSocialStatus(); } else { notify('Error al conectar la red social'); } setSocialConnecting(null); } } catch (_) { /* popup aún en dominio del proveedor */ } }, 600); // Cancelar tras 5 minutos si el usuario cierra sin completar setTimeout(() => { if (!popup.closed) popup.close(); clearInterval(interval); setSocialConnecting(null); }, 300000); } catch (_) { setSocialConnecting(null); } }; const handleSocialDisconnect = async (provider) => { const tk = token(); if (!tk) return; try { const res = await fetch(`/api/v1/social/disconnect/${provider}`, { method: 'DELETE', headers: { Authorization: 'Bearer ' + tk }, }); if (res.ok) { const provName = SOCIAL_PROVIDERS[provider]?.name || provider; notify(`${provName} desconectado`); loadSocialStatus(); } } catch (_) {} }; return (

Integraciones

administración {' '} {loading ? '…' : `${connected.length} conectadas · ${available.length} disponibles`}
{err && (
{err}
)} {/* ── Redes Sociales (OAuth personal) ── */}
Redes sociales OAuth personal por usuario
{Object.entries(SOCIAL_PROVIDERS).map(([provider, meta]) => { const conn = socialStatus.find(s => s.provider === provider); const isConnected = conn?.connected; const isConnecting = socialConnecting === provider; return (
{meta.label}
{meta.name}
{socialLoading ? '…' : isConnected ? `@${conn.username || 'conectado'}` : 'No conectado'}
{isConnected ? activa : disponible }

{meta.desc}

{isConnected ? ( ) : ( ) }
); })}
{/* ── Integraciones de workspace ── */} {/* Conectadas */} {!loading && connected.length > 0 && ( <>
Conectadas
{connected.map(i => (
{i.label}
{i.name}
{i.account_name || 'Cuenta conectada'}
{i.status === 'error' ? error : activa }
{i.scope === 'global_admin' ? 'Global/Admin' : 'Personal'} {i.requires_admin_approval && ( Aprobación admin )}
{/* Info test */} {i.last_tested_at && (
Test {i.last_test_ok ? 'OK' : 'KO'} · {fmtTested(i.last_tested_at)}
)}
{i.api_key_masked && ( {i.api_key_masked} )} {!i.api_key_masked && i.kind === 'webhook' && ( webhook )}
))}
)} {/* Skeleton de carga */} {loading && (
{[...Array(6)].map((_, i) => (
))}
)} {/* Disponibles */} {!loading && ( <>
0 ? 28 : 0 }}> Disponibles
{available.map(i => (
{i.label}
{i.name} {i.kind === 'webhook' ? 'webhook' : 'disponible'}
{i.scope === 'global_admin' ? 'Global/Admin' : 'Personal'} {i.requires_admin_approval && ( Aprobación admin )}
{i.desc && (

{i.desc}

)} {i.blocked_reason && (
{i.blocked_reason}
)} {i.reqs && i.reqs.length > 0 && (
Requisitos:{' '} {i.reqs.join(' · ')}
)} {i.deployTime && (
⏱ Despliegue: {i.deployTime}
)}
))}
)}
{/* Modal de conexión/configuración */} {modal && ( setModal(null)} onSaved={(updated) => { handleSaved(updated); setModal(null); }} /> )} {/* Modal de confirmación de desconexión */} {confirmDisconnect && ( setConfirmDisconnect(null)} onConfirm={() => handleDisconnect(confirmDisconnect)} /> )}
); }; window.cargarDetallesFacturacion = async function cargarDetallesFacturacion(clientId) { const container = document.getElementById('billing-dynamic-card'); if (!container) return; const tok = sessionStorage.getItem('af_token') || ''; const resolvedClientId = String( clientId || sessionStorage.getItem('af_customer_id') || (() => { try { const user = JSON.parse(sessionStorage.getItem('af_user') || 'null'); return user && user.id ? user.id : ''; } catch (_) { return ''; } })() ); if (!tok) { container.innerHTML = '
Sesión no válida.
'; return; } if (!resolvedClientId) { container.innerHTML = '
No se encontró el cliente en sesión.
'; return; } try { const res = await fetch('/api/v1/billing/details/' + encodeURIComponent(resolvedClientId), { headers: { Authorization: 'Bearer ' + tok }, }); if (!res.ok) { container.innerHTML = '
No se pudo cargar facturación.
'; return; } const data = await res.json(); const isActive = data.status === 'active'; const badgeClass = isActive ? 'ok' : 'bad'; const planLabel = String(data.plan || '').toLowerCase(); const periodEnd = data.current_period_end ? new Date(data.current_period_end).toLocaleDateString('es-ES') : '—'; const usedFmt = Number(data.tokens_used_this_month || 0).toLocaleString('es-ES'); const allowedFmt = Number(data.tokens_allowed || 0).toLocaleString('es-ES'); const pct = Math.max(0, Math.min(100, Number(data.tokens_usage_pct || 0))); container.innerHTML = `
Estado de suscripción
${planLabel.charAt(0).toUpperCase() + planLabel.slice(1)} ${data.status}
Renovación: ${periodEnd}
Uso mensual de tokens
${usedFmt} / ${allowedFmt}
${pct.toFixed(1)}%
Plan contratado
${data.is_paused ? 'Ejecuciones pausadas' : 'Ejecuciones habilitadas'}
`; const btn = document.getElementById('billing-plan-update-btn'); const sel = document.getElementById('billing-plan-select'); if (btn && sel) { btn.onclick = async () => { btn.disabled = true; btn.textContent = 'Actualizando...'; try { const putRes = await fetch('/api/v1/billing/details/' + encodeURIComponent(resolvedClientId) + '/plan', { method: 'PUT', headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json', }, body: JSON.stringify({ plan: sel.value }), }); if (!putRes.ok) { window.afNotify && window.afNotify('No se pudo actualizar el plan'); return; } window.afNotify && window.afNotify('Plan actualizado'); await window.cargarDetallesFacturacion(resolvedClientId); } finally { btn.disabled = false; btn.textContent = 'Actualizar'; } }; } } catch (_) { container.innerHTML = '
Error de red cargando facturación.
'; } }; const Billing = () => { const plans = [ { n: 'Starter', p: '97 €', per: 'agente / mes', d: 'Ideal para empezar a automatizar', f: ['1 agente activo', '5.000 ejecuciones/mes', 'Web chat embed', 'Logs 30 días', 'Soporte email 48 h', '1 canal (web)'], buy: '890 €', buyNote: 'Compra directa (sin suscripción)' }, { n: 'Pro', p: '297 €', per: 'agente / mes', d: 'Más volumen con mantenimiento incluido', featured: true, f: ['1 agente activo', '25.000 ejecuciones/mes', '3 canales (web, email, WhatsApp)', 'API + webhooks', 'Logs 90 días', 'Chat en vivo 12 h', 'Mantenimiento mensual incluido'], buy: '2.490 €', buyNote: 'Compra + mantenimiento 97 €/mes' }, { n: 'Enterprise', p: 'A medida', per: 'desde 497 €/mes por agente', d: 'Gobernanza, compliance y volumen', f: ['Agentes ilimitados', 'Ejecuciones sin límite', 'SSO + SCIM + auditoría', 'VPC dedicada · HIPAA', 'CSM + onboarding dedicado', 'SLA 99,95 % · soporte 24/7'], buy: null, buyNote: 'Proyecto a medida' }, ]; React.useEffect(() => { const token = sessionStorage.getItem('af_token') || ''; const customerId = sessionStorage.getItem('af_customer_id') || (() => { try { const user = JSON.parse(sessionStorage.getItem('af_user') || 'null'); return user && user.id ? String(user.id) : ''; } catch (_) { return ''; } })(); if (!token || !customerId) return; sessionStorage.setItem('af_customer_id', customerId); if (typeof window.cargarDetallesFacturacion === 'function') { window.cargarDetallesFacturacion(customerId); } }, []); return (

Facturación y planes

administración AgentForge · Pro · 149 €/mes
Cargando estado de suscripción...
Planes
{plans.map(p => (
{p.featured && ACTUAL}

{p.n}

{p.p} / {p.per}

{p.d}

    {p.f.map(f =>
  • {f}
  • )}
{p.buy && (
{p.buy} · {p.buyNote}
)}
))}
Uso por agente
{[ ['Triaje Nivel 1', 12840, '4,2 M', 38400, 142.18, 28], ['Revisor de PRs', 4280, '2,1 M', 8200, 88.40, 17], ['Cualificador de Leads', 1980, '0,9 M', 3960, 32.10, 6], ['Investigación Profunda', 612, '0,4 M', 2448, 21.80, 4], ['Cazador de Insights', 320, '0,1 M', 640, 12.04, 2], ].map((r,i) => ( ))}
AgenteEjec.TokensHerram.Coste% del plan
{r[0]} {r[1].toLocaleString('es-ES')} {r[2]} {r[3].toLocaleString('es-ES')} {r[4].toFixed(2).replace('.', ',')} €
{r[5]} %
); }; const TEAM_ROLE_META = { propietario: { label: 'Propietario', color: '#7c3aed', bg: 'rgba(124,58,237,0.15)' }, administrador:{ label: 'Administrador',color: '#f472b6', bg: 'rgba(244,114,182,0.15)' }, constructor: { label: 'Builder', color: '#34d399', bg: 'rgba(52,211,153,0.15)' }, revisor: { label: 'Revisor', color: '#60a5fa', bg: 'rgba(96,165,250,0.15)' }, lector: { label: 'Lector', color: '#fbbf24', bg: 'rgba(251,191,36,0.15)' }, operador_sensible: { label: 'Operador sensible', color: '#ef4444', bg: 'rgba(239,68,68,0.15)' }, }; const TEAM_ALL_ROLES = Object.keys(TEAM_ROLE_META); const TeamRolesPane = () => { const [members, setMembers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(null); // user id being saved const [showAdd, setShowAdd] = React.useState(false); const [addSearch, setAddSearch] = React.useState(''); const [allUsers, setAllUsers] = React.useState([]); const [usersLoading, setUsersLoading] = React.useState(false); const [showCreate, setShowCreate] = React.useState(false); const getToken = () => sessionStorage.getItem('af_token') || ''; React.useEffect(() => { (async () => { setLoading(true); try { const res = await fetch('/api/v1/admin/users', { headers: { Authorization: 'Bearer ' + getToken() } }); if (res.ok) setMembers(await res.json()); } catch (_) {} setLoading(false); })(); }, []); const addRole = async (userId, role) => { setSaving(userId); try { const res = await fetch('/api/v1/admin/users/' + userId + '/roles', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() }, body: JSON.stringify({ role }), }); if (res.ok) { const updatedRoles = await res.json(); setMembers(prev => prev.map(m => m.id === userId ? { ...m, roles: updatedRoles } : m)); } } catch (_) {} setSaving(null); }; const removeRole = async (userId, role) => { setSaving(userId); try { const res = await fetch('/api/v1/admin/users/' + userId + '/roles/' + role, { method: 'DELETE', headers: { Authorization: 'Bearer ' + getToken() }, }); if (res.ok) { const updatedRoles = await res.json(); setMembers(prev => prev.map(m => m.id === userId ? { ...m, roles: updatedRoles } : m)); } } catch (_) {} setSaving(null); }; const initials = (m) => (m.full_name || m.email || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); const topRoleColor = (m) => { if (m.is_admin) return '#ff5b1f'; const r = (m.roles || [])[0]; return r ? (TEAM_ROLE_META[r]?.color || '#888') : '#555'; }; return ( <>

Equipo y roles

{loading ? 'Cargando...' : `${members.length} miembro${members.length !== 1 ? 's' : ''}`}

{loading ? (
Cargando equipo...
) : (
{members.map(m => { const available = TEAM_ALL_ROLES.filter(r => !(m.roles || []).includes(r)); return (
{initials(m)}
{m.full_name || '—'}
{m.email}
{m.is_admin && Admin}
{/* Role badges */}
{(m.roles || []).map(r => { const meta = TEAM_ROLE_META[r] || { label: r, color: '#888', bg: 'rgba(255,255,255,0.05)' }; return ( {meta.label} ); })} {available.length > 0 && ( )}
); })}
)} {/* Añadir colaborador modal */} {showAdd && (
{ if (e.target === e.currentTarget) setShowAdd(false); }}>
Añadir colaborador
setAddSearch(e.target.value)} style={{ background:'#111', border:'1px solid #2a2a2a', borderRadius:6, padding:'8px 12px', fontSize:13, color:'#e5e5e5', fontFamily:'inherit', outline:'none', marginBottom:12 }} />
{usersLoading ? (
Cargando usuarios...
) : (() => { const q = addSearch.toLowerCase().trim(); const filtered = allUsers.filter(u => (u.email || '').toLowerCase().includes(q) || (u.full_name || '').toLowerCase().includes(q) ); if (filtered.length === 0) return
Sin resultados.
; return filtered.map(u => { const ini = (u.full_name || u.email || '?').split(' ').map(w => w[0]).join('').slice(0,2).toUpperCase(); const topRole = (u.roles || [])[0]; const roleColor = topRole ? (TEAM_ROLE_META[topRole]?.color || '#888') : '#555'; return (
{ if (!members.find(m => m.id === u.id)) setMembers(prev => [...prev, u]); setShowAdd(false); }} style={{ display:'flex', alignItems:'center', gap:10, padding:'9px 8px', borderRadius:7, cursor:'pointer', marginBottom:2 }} onMouseEnter={e => e.currentTarget.style.background='#222'} onMouseLeave={e => e.currentTarget.style.background='transparent'} >
{ini}
{u.full_name || '—'}
{u.email}
{(u.roles || []).length > 0 && ( {TEAM_ROLE_META[(u.roles||[])[0]]?.label || u.roles[0]} )}
); }); })()}
)} {showCreate && ( setShowCreate(false)} onSuccess={(u) => { setMembers(prev => [...prev, u]); setShowCreate(false); }} /> )} ); }; const MfaManager = () => { const me = (() => { try { return JSON.parse(sessionStorage.getItem('af_user')); } catch { return null; } })(); const token = sessionStorage.getItem('af_token'); const [mfaEnabled, setMfaEnabled] = React.useState(me?.mfa_enabled || false); const [setupMode, setSetupMode] = React.useState(false); const [qrSvg, setQrSvg] = React.useState(''); const [secret, setSecret] = React.useState(''); const [activateCode, setActivateCode] = React.useState(''); const [disableMode, setDisableMode] = React.useState(false); const [disableCode, setDisableCode] = React.useState(''); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(''); const [ok, setOk] = React.useState(''); const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; const startSetup = async () => { setLoading(true); setErr(''); setOk(''); try { const res = await fetch('/api/v1/customers/mfa/setup', { method: 'POST', headers }); const data = await res.json(); if (!res.ok) { setErr(data.detail || 'Error al inicializar MFA'); setLoading(false); return; } setQrSvg(data.qr_svg); setSecret(data.secret); setSetupMode(true); } catch (_) { setErr('Error de red.'); } setLoading(false); }; const activate = async () => { if (activateCode.length !== 6) { setErr('Introduce el código de 6 dígitos'); return; } setLoading(true); setErr(''); try { const res = await fetch('/api/v1/customers/mfa/activate', { method: 'POST', headers, body: JSON.stringify({ code: activateCode }), }); const data = await res.json(); if (!res.ok) { setErr(data.detail || 'Código incorrecto'); setLoading(false); return; } setMfaEnabled(true); setSetupMode(false); setActivateCode(''); setOk('MFA activado correctamente.'); if (me) sessionStorage.setItem('af_user', JSON.stringify({ ...me, mfa_enabled: true })); } catch (_) { setErr('Error de red.'); } setLoading(false); }; const disable = async () => { if (disableCode.length !== 6) { setErr('Introduce el código de 6 dígitos'); return; } setLoading(true); setErr(''); try { const res = await fetch('/api/v1/customers/mfa/disable', { method: 'POST', headers, body: JSON.stringify({ code: disableCode }), }); const data = await res.json(); if (!res.ok) { setErr(data.detail || 'Código incorrecto'); setLoading(false); return; } setMfaEnabled(false); setDisableMode(false); setDisableCode(''); setOk('MFA desactivado.'); if (me) sessionStorage.setItem('af_user', JSON.stringify({ ...me, mfa_enabled: false })); } catch (_) { setErr('Error de red.'); } setLoading(false); }; if (!me || !token) return (

Inicia sesión para gestionar MFA.

); return (
Autenticación de dos factores (MFA)

Protege tu cuenta con Google Authenticator, Authy u otra app TOTP.

{mfaEnabled ? Activo : Inactivo } {mfaEnabled ? : }
{ok &&
{ok}
} {err &&
{err}
} {setupMode && (

1. Escanea este QR con tu app de autenticación

¿No puedes escanear? Introduce esta clave manualmente:

{secret}

2. Introduce el código de 6 dígitos para confirmar:

{ setActivateCode(e.target.value.replace(/\D/g, '')); setErr(''); }} autoFocus />
)} {disableMode && (

Confirma con tu código MFA para desactivar

{ setDisableCode(e.target.value.replace(/\D/g, '')); setErr(''); }} autoFocus />
)}
); }; const SettingsInner = () => { const [pane, setPane] = React.useState('general'); const [saving, setSaving] = React.useState(false); const [settingsForm, setSettingsForm] = React.useState({ workspace_name: 'AgentForge-IA', default_region: 'eu-south', default_model: 'sonnet', theme: 'dark', team_invite_domain: 'agentforge-ia.pro', team_auto_assign_reviewer: true, security_sso_enabled: true, security_scim_enabled: true, security_require_mfa_org: true, security_ip_whitelist_enabled: true, security_secret_rotation_days: 90, governance_predeploy_eval_gate: true, governance_human_approval_prod: true, governance_pii_anonymization: true, governance_tool_allowlist: false, governance_eval_cadence: 'hourly', compute_default_runtime: 'shared', compute_max_concurrency: 10, compute_rate_limit_rpm: 120, api_enabled: true, api_default_scope: 'read', audit_retention_days: 180, audit_stream_webhook: '', notifs_email_enabled: true, notifs_daily_digest: true, notifs_alert_channel: 'email', }); const STORAGE_KEY = 'af_admin_settings_v1'; React.useEffect(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return; setSettingsForm((prev) => ({ ...prev, ...parsed })); } catch (_) {} }, []); const saveSettings = async () => { setSaving(true); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settingsForm)); window.afNotify && window.afNotify('Configuración guardada'); } catch (_) { window.alert('No se pudo guardar la configuración.'); } finally { setSaving(false); } }; const Toggle = ({ checked, onChange }) => ( ))}
{pane === 'general' && ( <>

General

Identidad del espacio de trabajo y valores por defecto.

Nombre del espacio

Aparece en la barra superior

setSettingsForm((prev) => ({ ...prev, workspace_name: e.target.value }))} style={{ width: 240 }} />
Región por defecto

Dónde se despliegan los agentes nuevos

Modelo por defecto

Para agentes nuevos salvo que se sobrescriba

Tema

Claro / oscuro / sistema

)} {pane === 'team' && } {pane === 'security' && ( <>

Seguridad

MFA personal, SSO, listas blancas de IP, gestión de secretos.

SSO (SAML)

Okta · obligatorio para no-propietarios

setSettingsForm((p) => ({ ...p, security_sso_enabled: v }))} />
Aprovisionamiento SCIM

Sincronización automática de grupos del directorio

setSettingsForm((p) => ({ ...p, security_scim_enabled: v }))} />
Exigir MFA (org)

Para todos los roles builder y administrador

setSettingsForm((p) => ({ ...p, security_require_mfa_org: v }))} />
Lista blanca de IP

Activar control de acceso por rangos

setSettingsForm((p) => ({ ...p, security_ip_whitelist_enabled: v }))} />
Rotación de secretos (días)

Rotar claves API automáticamente

setSettingsForm((p) => ({ ...p, security_secret_rotation_days: Number(e.target.value || 90) }))} style={{ width: 120 }} />
)} {pane === 'governance' && ( <>

Gobernanza y evals

Compuertas previas al despliegue y evaluaciones continuas.

Compuerta de eval antes de desplegar

Mínimo 95 % de aprobado en el set dorado

setSettingsForm((p) => ({ ...p, governance_predeploy_eval_gate: v }))} />
Aprobación humana para producción

Obligatoria en el primer despliegue

setSettingsForm((p) => ({ ...p, governance_human_approval_prod: v }))} />
Anonimización de PII en logs

Eliminar emails, nombres, tarjetas

setSettingsForm((p) => ({ ...p, governance_pii_anonymization: v }))} />
Lista blanca de herramientas

Bloquear herramientas no permitidas en el espacio

setSettingsForm((p) => ({ ...p, governance_tool_allowlist: v }))} />
Cadencia de eval continua

Cada cuánto se evalúan las ejecuciones en sombra

)} {pane === 'compute' && (

Cómputo y regiones

Configura regiones de ejecución y topes operativos.

Runtime por defecto

Modo de ejecución para nuevos agentes

Concurrencia máxima

Número simultáneo de ejecuciones

setSettingsForm((p) => ({ ...p, compute_max_concurrency: Number(e.target.value || 10) }))} style={{ width: 120 }} />
Rate limit (RPM)

Tope de peticiones por minuto

setSettingsForm((p) => ({ ...p, compute_rate_limit_rpm: Number(e.target.value || 120) }))} style={{ width: 120 }} />
)} {pane === 'api' && (

Claves API

Políticas de acceso programático.

API habilitada

Permite uso de tokens de API

setSettingsForm((p) => ({ ...p, api_enabled: v }))} />
Scope por defecto

Permisos para nuevas claves

)} {pane === 'audit' && (

Registro de auditoría

Retención y salida de eventos.

Retención (días)

Tiempo de conservación del log

setSettingsForm((p) => ({ ...p, audit_retention_days: Number(e.target.value || 180) }))} style={{ width: 120 }} />
Webhook de auditoría

Destino opcional para streaming

setSettingsForm((p) => ({ ...p, audit_stream_webhook: e.target.value }))} placeholder="https://..." style={{ width: 320 }} />
)} {pane === 'notifs' && (

Notificaciones

Canales y preferencias de alertas.

Email habilitado

Permite envío de alertas por email

setSettingsForm((p) => ({ ...p, notifs_email_enabled: v }))} />
Resumen diario

Enviar digest diario automático

setSettingsForm((p) => ({ ...p, notifs_daily_digest: v }))} />
Canal principal

Destino por defecto de alertas críticas

)}
); }; const Settings = () => (

Ajustes

administración AgentForge
); const Home = ({ data, setRoute, openAgent }) => { const [liveFeed, setLiveFeed] = React.useState([]); const [subscription, setSubscription] = React.useState(null); const [recentRuns, setRecentRuns] = React.useState(null); const [userName, setUserName] = React.useState(''); const token = sessionStorage.getItem('af_token') || ''; React.useEffect(() => { if (!token) return; let fallback = ''; try { const payload = JSON.parse(atob(token.split('.')[1])); const email = payload.sub || payload.email || ''; const raw = email.includes('@') ? email.split('@')[0] : email; fallback = raw.charAt(0).toUpperCase() + raw.slice(1).replace(/[._-]/g, ' '); } catch (_) {} fetch('/api/v1/customers/me', { headers: { Authorization: 'Bearer ' + token } }) .then((r) => (r.ok ? r.json() : null)) .then((me) => { const fullName = (me && (me.full_name || me.fullName)) ? String(me.full_name || me.fullName).trim() : ''; setUserName(fullName || fallback); }) .catch(() => { if (fallback) setUserName(fallback); }); }, [token]); React.useEffect(() => { if (!token) return; fetch('/api/v1/billing/my-subscription', { headers: { Authorization: 'Bearer ' + token } }) .then(r => r.ok ? r.json() : null) .then(d => d && setSubscription(d)) .catch(() => {}); }, [token]); React.useEffect(() => { if (!token) return; fetch('/api/v1/runs?limit=5', { headers: { Authorization: 'Bearer ' + token } }) .then(r => r.ok ? r.json() : []) .then(d => setRecentRuns(Array.isArray(d) ? d : [])) .catch(() => setRecentRuns([])); }, [token]); React.useEffect(() => { if (!token) return; let mounted = true; const loadFeed = async () => { try { const res = await fetch('/api/v1/agents/live-feed?limit=10', { headers: { Authorization: 'Bearer ' + token } }); if (!res.ok) return; const payload = await res.json(); const rows = Array.isArray(payload.items) ? payload.items : []; const entries = rows.map((x) => { const agent = (x.agent_name || 'Agente').toString(); const snippet = (x.snippet || 'Actividad reciente').toString(); const tok = Number(x.tokens || 0).toLocaleString('es-ES'); return `${agent}: ${snippet} · ${tok} tok`; }).filter(Boolean); if (mounted && entries.length) setLiveFeed(entries); } catch (_) {} }; loadFeed(); const timer = setInterval(loadFeed, 20000); return () => { mounted = false; clearInterval(timer); }; }, [token]); const agents = Array.isArray(data?.agents) ? data.agents : []; const cats = Array.isArray(data?.categories) ? data.categories : []; const tokensUsed = subscription?.tokens_used_this_month || 0; const tokensLimit = subscription?.tokens_allowed || 500000; const usagePct = Math.min(100, Math.round((tokensUsed / Math.max(tokensLimit, 1)) * 100)); const planLabel = { starter: 'Starter', professional: 'Professional', enterprise: 'Enterprise' }[subscription?.plan] || subscription?.plan || ''; const greeting = (() => { const h = new Date().getHours(); return h < 12 ? 'Buenos días' : h < 20 ? 'Buenas tardes' : 'Buenas noches'; })(); const quickStart = [ { id: '1', title: '1) Explora y elige agente', desc: 'Filtra por categoría y abre la ficha para entender su caso de uso.', cta: 'Ir a Marketplace', route: 'marketplace', icon: 'store', eta: '1 min' }, { id: '2', title: '2) Prueba en entorno seguro', desc: 'Valida respuestas, tono y trazas antes de desplegar en producción.', cta: 'Abrir Pruebas', route: 'playground', icon: 'play', eta: '2 min' }, { id: '3', title: '3) Ajusta flujo en Editor', desc: 'Conecta nodos, herramientas y memoria para adaptar el agente a tu negocio.', cta: 'Abrir Editor', route: 'builder', icon: 'flow', eta: '3 min' }, { id: '4', title: '4) Despliega y monitoriza', desc: 'Activa el agente y sigue su rendimiento en Ejecuciones y Analítica.', cta: 'Ir a Implantación', route: 'deployment', icon: 'cloud', eta: '2 min' }, ]; return (

Inicio

{/* ── HERO BANNER ── */}
{greeting}
{userName || 'Tu espacio de trabajo'}
Tu plataforma de agentes IA está operativa
{planLabel && Plan {planLabel}} {subscription?.status === 'active' && ( Activo )} {subscription?.is_paused && Pausado} {subscription && !subscription.has_subscription && Sin plan}
{agents.length}
agentes disponibles
{(tokensUsed / 1000).toFixed(1)}k
tokens este mes
90 ? 'var(--bad)' : usagePct > 70 ? 'var(--warn)' : 'var(--ok)' }}> {usagePct}%
cuota usada
{(tokensUsed / 1000).toFixed(1)}k / {(tokensLimit / 1000).toFixed(0)}k
90 ? 'var(--bad)' : usagePct > 70 ? 'var(--warn)' : 'var(--brand)', transition: 'width 0.5s' }} />
setRoute('runs')}>
{recentRuns === null ? '—' : recentRuns.length}
ejecuciones recientes
{/* ── MAIN GRID: timeline + panel lateral ── */}
{/* Actividad reciente — timeline */}
Actividad reciente
{recentRuns && recentRuns.length > 0 && ( )}
{recentRuns === null ? (
{[1, 2, 3, 4, 5].map(i => (
))}
) : recentRuns.length === 0 ? (
Sin ejecuciones todavía
Prueba un agente para ver la actividad aquí
) : (
{recentRuns.slice(0, 5).map((run, i) => { const borderCol = { ok: 'var(--ok)', error: 'var(--bad)', warning: 'var(--warn)' }[run.status] || 'var(--line-3)'; const ts = run.created_at ? new Date(run.created_at).toLocaleString('es-ES', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'; const dur = run.duration_ms ? `${(run.duration_ms / 1000).toFixed(1)} s` : null; return (
{run.agent_name || '—'}
{ts}{dur ? ` · ${dur}` : ''} · {(run.tokens_used || 0).toLocaleString('es-ES')} tok
{run.cost_eur > 0 && {run.cost_eur.toFixed(2)} €}
); })}
)}
{/* Panel lateral: acciones rápidas + feed en vivo */}
Acciones rápidas
{[ { route: 'marketplace', icon: 'store', label: 'Explorar catálogo', sub: `${agents.length} agentes disponibles`, bg: 'var(--brand-soft)', color: 'var(--brand)' }, { route: 'playground', icon: 'play', label: 'Área de pruebas', sub: 'Valida antes de desplegar', bg: 'color-mix(in oklab, var(--violet) 15%, var(--bg-2))', color: 'var(--violet)' }, { route: 'builder', icon: 'flow', label: 'Editor de flujos', sub: 'Conecta nodos y herramientas', bg: 'color-mix(in oklab, var(--teal) 15%, var(--bg-2))', color: 'var(--teal)' }, { route: 'deployment', icon: 'cloud', label: 'Implantación', sub: 'Activa y monitoriza agentes', bg: 'color-mix(in oklab, var(--info) 15%, var(--bg-2))', color: 'var(--info)' }, ].map(({ route, icon, label, sub, bg, color }) => ( ))}
{liveFeed.length > 0 && (
Feed en vivo
{liveFeed.slice(0, 5).map((entry, idx) => { const colonIdx = entry.indexOf(': '); const agentPart = colonIdx > -1 ? entry.slice(0, colonIdx) : entry; const rest = colonIdx > -1 ? entry.slice(colonIdx + 2) : ''; return (
{agentPart} {rest && {rest}}
); })}
)}
{/* ── AGENTES SUGERIDOS ── */}
Sugeridos para tu equipo
{[...agents].sort((a, b) => (Number(b.runs || 0) - Number(a.runs || 0))).slice(0, 4).map((a) => { const cat = cats.find(c => c.id === a.cat); return openAgent(a.id)} />; })}
{/* ── CÓMO EMPEZAR ── */}
Cómo empezar
{quickStart.map((step) => (
{step.id}
{step.eta}
{step.title}
{step.desc}
))}
); }; // --------------------------------------------------------------------------- // RunDetail — panel lateral de detalle editable // --------------------------------------------------------------------------- const RunDetail = ({ run, onClose, onSaved }) => { const [notes, setNotes] = React.useState(run.notes || ''); const [status, setStatus] = React.useState(run.status); const [saving, setSaving] = React.useState(false); const [saved, setSaved] = React.useState(false); const token = () => sessionStorage.getItem('af_token') || ''; const fmtFull = (iso) => { if (!iso) return '—'; return new Date(iso).toLocaleString('es-ES', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', }); }; const handleSave = async () => { setSaving(true); try { const res = await fetch(`/api/v1/runs/${run.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token() }, body: JSON.stringify({ notes: notes.trim() || null, status }), }); if (res.ok) { const updated = await res.json(); setSaved(true); setTimeout(() => setSaved(false), 2000); onSaved && onSaved(updated); } } catch (_) {} setSaving(false); }; const statusColor = { ok: 'var(--ok)', error: 'var(--bad)', warning: 'var(--warn)', skipped: 'var(--ink-3)' }; return (
Detalle de ejecución
{run.id}
{/* Meta */}
{[ ['Agente', run.agent_name], ['Disparador', run.trigger_type], ['Duración', run.duration_ms != null ? (run.duration_ms / 1000).toFixed(3) + ' s' : '—'], ['Tokens', (run.tokens_used || 0).toLocaleString('es-ES')], ['Coste', Number(run.cost_eur || 0).toFixed(4) + ' €'], ['Fecha', fmtFull(run.created_at)], ].map(([k, v]) => (
{k}
{v}
))}
{/* Estado editable */}
Estado
{['ok', 'error', 'warning', 'skipped'].map(s => ( ))}
{/* Error message */} {run.error_message && (
Mensaje de error
{run.error_message}
)} {/* Notas editables */}
Notas internas