Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
215 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "asolache/teamtowershuma", "files": {"v8/js/views/DashboardView.js": {"7cc164f249d2b8c8c764637cd59553885bd46053": {"t": 1785746023.1638975, "n": 33099, "h": "1bc69e8b79dda86a", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { KB } from '../core/kb.js'; // Importamos KB para la integración RAG si fuera necesaria\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\nimport { MapRenderer } from '../components/MapRenderer.js'; \nimport { LedgerRenderer } from '../components/LedgerRenderer.js'; \n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V14\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V14 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience ? store.calculateResilience(project.id) : 100;\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- CÁLCULO DE TELEMETRÍA (Arbitraje IA) ---\n let aiGrossValue = 0; \n let realApiCost = 0; \n let totalTokens = 0;\n\n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * (tx.horas || 1); \n }\n });\n\n if (project.telemetry && project.telemetry.length > 0) {\n project.telemetry.forEach(log => {\n realApiCost += log.costInDollars || 0;\n totalTokens += (log.tokens?.total_tokens || log.tokens?.prompt_tokens + log.tokens?.completion_tokens) || 0;\n });\n }\n const recRatio = realApiCost > 0 ? (aiGrossValue / realApiCost) : 0;\n \n // --- IDENTIFICACIÓN DEL ENJAMBRE IA ASIGNADO ---\n const projectUsers = project.usuarios || [];\n const aisInProject = projectUsers.map(u => state.globalUsers.find(gu => gu.id === u.id)).filter(u => u && u.profile?.isAi);\n const aiAvatarsHtml = aisInProject.length > 0 \n ? aisInProject.map(ai => `<div class=\"ai-avatar-badge\" title=\"${ai.name} (${ai.id})\">🤖</div>`).join('')\n : `<div style=\"color:#888; font-size:0.8rem; font-style:italic;\">No hay Agentes IA en la Colla.</div>`;\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Ojo del Castell (Centro de Mando)\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n ${MapRenderer.getStyles()}\n ${LedgerRenderer.getStyles()}\n\n .app-layout { display: flex; height: 100vh; overflow: hidden; background: var(--bg-dark); font-family: var(--font-main); width: 100%;}\n .workspace-dash { flex: 1; padding: 2rem 3rem; overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth; box-sizing: border-box;}\n \n .tab-content { display: none; animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); padding-bottom: 5rem; width: 100%; box-sizing: border-box;}\n .tab-content.active { display: block; }\n\n /* GRID PRINCIPAL DEL DASHBOARD */\n .dash-grid { display: grid; grid-template-columns: 1fr 350px; gap: 2rem; margin-bottom: 2.5rem; align-items: start;}\n \n /* PANELES DE INFORMACIÓN */\n .dash-panel { background: linear-gradient(145deg, rgba(20,20,25,0.8), rgba(10,10,15,0.9)); border: 1px solid var(--glass-border); border-radius: 24px; padding: 2rem; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 10px 30px rgba(0,0,0,0.5); backdrop-filter: blur(15px); display: flex; flex-direction: column; height: 100%; box-sizing: border-box;}\n .panel-title { color: white; font-size: 1.2rem; font-weight: 900; margin-top: 0; margin-bottom: 1.5rem; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; display:flex; justify-content:space-between; align-items:center; text-transform:uppercase; letter-spacing:1px;}\n \n /* PRESENTACIÓN Y ENLACE AL PAPER */\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; font-style: italic; border-left: 3px solid var(--accent-purple); padding-left: 15px; margin-bottom: 1.5rem; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;}\n .btn-open-paper { background: linear-gradient(135deg, rgba(224,64,251,0.1), rgba(0,176,255,0.1)); border: 1px solid var(--accent-purple); color: white; padding: 12px 20px; border-radius: 12px; font-weight: bold; cursor: pointer; transition: 0.3s; display:flex; justify-content:center; align-items:center; gap:10px; width: 100%; font-size: 0.95rem;}\n .btn-open-paper:hover { background: var(--accent-purple); box-shadow: 0 0 20px rgba(224,64,251,0.4); transform: translateY(-2px);}\n\n /* MINI MAP CONTAINER DRY */\n .mini-map-container { width: 100%; height: 300px; position: relative; border-radius:16px; border:1px solid #333; overflow:hidden; margin-bottom: 2rem;}\n\n /* KPIS */\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* TELEMETRÍA A2A */\n .ai-arbitrage-panel { background: rgba(0,0,0,0.4); border: 1px dashed var(--accent-blue); padding: 1.5rem; border-radius: 16px; display: flex; flex-direction:column; gap: 15px;}\n .ai-stat-row { display: flex; align-items: center; justify-content: space-between;}\n .ai-stat-lbl { font-size: 0.7rem; color: #aaa; text-transform: uppercase; font-weight: bold;}\n .ai-stat-val { font-size: 1.4rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n .ai-avatar-badge { width: 35px; height: 35px; border-radius: 50%; background: var(--accent-purple); display:flex; justify-content:center; align-items:center; font-size: 1.2rem; border: 2px solid #111; margin-left: -10px; box-shadow: 0 2px 5px rgba(0,0,0,0.5);}\n .ai-avatar-badge:first-child { margin-left: 0; }\n\n /* MODAL IA */\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 1024px) {\n .dash-grid { grid-template-columns: 1fr; }\n }\n @media (max-width: 768px) {\n .workspace-dash { padding: 90px 1rem 120px 1rem; }\n .dash-panel { padding: 1.5rem; }\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace-dash\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"dash-grid\">\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-purple); margin-bottom: 2rem;\">\n <div class=\"panel-title\"><span>📖 Propósito Fundacional</span> <div style=\"display:flex; gap:5px;\">${tagsHtml}</div></div>\n <div class=\"presentation-text\" id=\"pitchText\">${pitchText}</div>\n <button class=\"btn-open-paper\" id=\"btnOpenPaper\">📝 Desarrollar en Omni-Paper (Usenet)</button>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-blue); padding:0;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">🕸️ Topología del Ecosistema</div>\n <div class=\"mini-map-container\" id=\"dashMapContainer\">\n <div class=\"map-canvas map-svg-layer\" id=\"dashMapCanvas\">\n <svg id=\"dashMapSvg\">\n <defs>\n <marker id=\"arrow-tangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#00e676\"/></marker>\n <marker id=\"arrow-intangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#e040fb\"/></marker>\n </defs>\n <g id=\"dashMapPaths\"></g>\n </svg>\n </div>\n </div>\n <div style=\"padding: 0 2rem 2rem 2rem;\">\n <a href=\"/v8/map\" data-link style=\"color:var(--accent-blue); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Modificar Arquitectura &rarr;</a>\n </div>\n </div>\n </div>\n\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); margin-bottom: 2rem;\">\n <div class=\"panel-title\">🤖 Enjambre IA Activo</div>\n <div style=\"display:flex; margin-bottom:1rem;\">\n ${aiAvatarsHtml}\n </div>\n \n <div class=\"ai-arbitrage-panel\">\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Tokens Consumidos</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-blue);\">${(totalTokens / 1000).toFixed(1)}k</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Gasto API (USD)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">$${realApiCost.toFixed(4)}</span>\n </div>\n <div class=\"ai-stat-row\" style=\"border-top: 1px dashed #444; padding-top: 15px; margin-top: 5px;\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-green);\">Valor (Slices) IA</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-green);\">€${aiGrossValue.toLocaleString()}</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-purple);\">Ratio (REC)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-purple);\">${recRatio > 0 ? recRatio.toFixed(0) + 'x' : '0'}</span>\n </div>\n </div>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); padding:0; overflow:hidden;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">⚖️ Distribución de Equidad</div>\n <div id=\"dashLedgerContainer\" style=\"padding: 0 2rem 2rem 2rem;\"></div>\n <div style=\"background: rgba(0,230,118,0.1); padding: 15px 2rem; text-align:right;\">\n <a href=\"/v8/ledger\" data-link style=\"color:var(--accent-green); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Ir a la Notaría (Ledger Completo) &rarr;</a>\n </div>\n </div>\n\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem;\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.95rem; margin-bottom:2rem; line-height:1.5;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado. El <b>Dharma Coach</b> usa esto para reclutar talento.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem; text-align:center;\">\n <h2 style=\"color: var(--text-muted); margin-top:0;\">Módulo de Configuración de Red</h2>\n <p style=\"color:#888;\">Las opciones de gobernanza, APIs multimodales y Padrón de IAs se gestionan de forma segura y Zero-Trust desde la Consola Global.</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:2rem; text-decoration:none;\">Ir al Panteón Global</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none; background:var(--accent-purple); color:white; border:none; padding:10px 20px; border-radius:12px; font-weight:bold; cursor:pointer;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const project = state.projects.find(p => p.id === this.activeProjectId);\n if(!project) return;\n\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // 🔥 MAGIA DRY 1: RENDERIZAR MAPA EN EL DASHBOARD\n const dashMapCanvas = document.getElementById('dashMapCanvas');\n const dashMapPaths = document.getElementById('dashMapPaths');\n if (dashMapCanvas && dashMapPaths && project.roles) {\n const mr = new MapRenderer(dashMapCanvas, dashMapPaths, { \n isMacro: true, \n isHeatmap: false,\n markerSuffix: 'vis',\n trimSize: 20\n });\n mr.setData(project.roles, project.vna_flows || []);\n }\n\n // 🔥 MAGIA DRY 2: RENDERIZAR LEDGER (SOLO PIE Y CAP TABLE)\n const dashLedgerContainer = document.getElementById('dashLedgerContainer');\n if (dashLedgerContainer) {\n const lr = new LedgerRenderer(dashLedgerContainer, {\n projectId: project.id,\n showHistory: false // Ocultamos la tabla de Blockchain para no saturar el Dashboard\n });\n lr.render();\n }\n\n // 🔥 CREACIÓN DINÁMICA DE WORK ORDER (IR AL PAPER)\n const btnOpenPaper = document.getElementById('btnOpenPaper');\n if (btnOpenPaper) {\n btnOpenPaper.addEventListener('click', async () => {\n // Creamos una Work Order \"Teórica\" en el vuelo (Una Tarea Huérfana de investigación)\n const newHash = 'wo_research_' + Math.random().toString(36).substr(2, 9);\n \n await store.dispatch({\n type: 'SPAWN_WORK_ORDER',\n payload: {\n projectId: this.activeProjectId,\n workOrder: {\n hash: newHash, \n flowId: null, // No pertenece a ningún SOP, es investigación libre\n comentario: `Investigación y Desarrollo del Manifiesto / Prompt del proyecto.`,\n status: 'pinged', // Directamente en curso\n realHours: 0,\n sprintId: project.activeSprintId,\n soc_checklist: [], \n assigneeId: state.session.activeUserId // Asignada al PO\n }\n }\n });\n\n // Redirigimos al Omni-Paper con el Hash cargado\n window.location.href = `/v8/paper?hash=${newHash}`;\n });\n }\n\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Alias (@id):`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // Lógica Modal IA (Auditoría VNA & Equity)\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n const modalCloseBtn = document.getElementById('aiModalClose');\n\n if (modalCloseBtn) modalCloseBtn.onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n if (!modal) return;\n \n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en la Consola V14 antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Analizando el Ledger Fractal y la Topología...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = store.getState().globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Nodo: ${userName} | Equidad: ${((h.slices / totalSlices) * 100).toFixed(2)}%`;\n });\n }\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n roles: project.roles.map(r => r.name),\n cap_table_actual: capTableDetails\n };\n\n let systemPrompt = \"Eres un Master Architect de DAOs.\";\n if (type === 'audit') {\n systemPrompt += `\\nEvalúa si la topología y distribución de Slices refleja un ecosistema sano basado en la teoría Slicing Pie.`;\n } else {\n systemPrompt += `\\nRedacta un Pacto de Socios formal (Smart Contract / Legal Draft) basado en el Cap Table adjunto.`;\n }\n\n try {\n // LLamada Legacy API directa (Refactor inminente a Orchestrator en Sprints futuros)\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] }) });\n text = (await res.json()).candidates[0].content.parts[0].text;\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const a = document.createElement('a'); \n a.href = URL.createObjectURL(new Blob([text], {type:\"text/plain\"})); \n a.download = `${type}_${project.nombre.replace(/ /g,'_')}.txt`; \n a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "5e32010e0ce8e899461d3d220d3c09c90401d155": {"t": 1785758898.5011497, "n": 33099, "h": "1bc69e8b79dda86a", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { KB } from '../core/kb.js'; // Importamos KB para la integración RAG si fuera necesaria\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\nimport { MapRenderer } from '../components/MapRenderer.js'; \nimport { LedgerRenderer } from '../components/LedgerRenderer.js'; \n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V14\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V14 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience ? store.calculateResilience(project.id) : 100;\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- CÁLCULO DE TELEMETRÍA (Arbitraje IA) ---\n let aiGrossValue = 0; \n let realApiCost = 0; \n let totalTokens = 0;\n\n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * (tx.horas || 1); \n }\n });\n\n if (project.telemetry && project.telemetry.length > 0) {\n project.telemetry.forEach(log => {\n realApiCost += log.costInDollars || 0;\n totalTokens += (log.tokens?.total_tokens || log.tokens?.prompt_tokens + log.tokens?.completion_tokens) || 0;\n });\n }\n const recRatio = realApiCost > 0 ? (aiGrossValue / realApiCost) : 0;\n \n // --- IDENTIFICACIÓN DEL ENJAMBRE IA ASIGNADO ---\n const projectUsers = project.usuarios || [];\n const aisInProject = projectUsers.map(u => state.globalUsers.find(gu => gu.id === u.id)).filter(u => u && u.profile?.isAi);\n const aiAvatarsHtml = aisInProject.length > 0 \n ? aisInProject.map(ai => `<div class=\"ai-avatar-badge\" title=\"${ai.name} (${ai.id})\">🤖</div>`).join('')\n : `<div style=\"color:#888; font-size:0.8rem; font-style:italic;\">No hay Agentes IA en la Colla.</div>`;\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Ojo del Castell (Centro de Mando)\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n ${MapRenderer.getStyles()}\n ${LedgerRenderer.getStyles()}\n\n .app-layout { display: flex; height: 100vh; overflow: hidden; background: var(--bg-dark); font-family: var(--font-main); width: 100%;}\n .workspace-dash { flex: 1; padding: 2rem 3rem; overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth; box-sizing: border-box;}\n \n .tab-content { display: none; animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); padding-bottom: 5rem; width: 100%; box-sizing: border-box;}\n .tab-content.active { display: block; }\n\n /* GRID PRINCIPAL DEL DASHBOARD */\n .dash-grid { display: grid; grid-template-columns: 1fr 350px; gap: 2rem; margin-bottom: 2.5rem; align-items: start;}\n \n /* PANELES DE INFORMACIÓN */\n .dash-panel { background: linear-gradient(145deg, rgba(20,20,25,0.8), rgba(10,10,15,0.9)); border: 1px solid var(--glass-border); border-radius: 24px; padding: 2rem; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 10px 30px rgba(0,0,0,0.5); backdrop-filter: blur(15px); display: flex; flex-direction: column; height: 100%; box-sizing: border-box;}\n .panel-title { color: white; font-size: 1.2rem; font-weight: 900; margin-top: 0; margin-bottom: 1.5rem; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; display:flex; justify-content:space-between; align-items:center; text-transform:uppercase; letter-spacing:1px;}\n \n /* PRESENTACIÓN Y ENLACE AL PAPER */\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; font-style: italic; border-left: 3px solid var(--accent-purple); padding-left: 15px; margin-bottom: 1.5rem; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;}\n .btn-open-paper { background: linear-gradient(135deg, rgba(224,64,251,0.1), rgba(0,176,255,0.1)); border: 1px solid var(--accent-purple); color: white; padding: 12px 20px; border-radius: 12px; font-weight: bold; cursor: pointer; transition: 0.3s; display:flex; justify-content:center; align-items:center; gap:10px; width: 100%; font-size: 0.95rem;}\n .btn-open-paper:hover { background: var(--accent-purple); box-shadow: 0 0 20px rgba(224,64,251,0.4); transform: translateY(-2px);}\n\n /* MINI MAP CONTAINER DRY */\n .mini-map-container { width: 100%; height: 300px; position: relative; border-radius:16px; border:1px solid #333; overflow:hidden; margin-bottom: 2rem;}\n\n /* KPIS */\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* TELEMETRÍA A2A */\n .ai-arbitrage-panel { background: rgba(0,0,0,0.4); border: 1px dashed var(--accent-blue); padding: 1.5rem; border-radius: 16px; display: flex; flex-direction:column; gap: 15px;}\n .ai-stat-row { display: flex; align-items: center; justify-content: space-between;}\n .ai-stat-lbl { font-size: 0.7rem; color: #aaa; text-transform: uppercase; font-weight: bold;}\n .ai-stat-val { font-size: 1.4rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n .ai-avatar-badge { width: 35px; height: 35px; border-radius: 50%; background: var(--accent-purple); display:flex; justify-content:center; align-items:center; font-size: 1.2rem; border: 2px solid #111; margin-left: -10px; box-shadow: 0 2px 5px rgba(0,0,0,0.5);}\n .ai-avatar-badge:first-child { margin-left: 0; }\n\n /* MODAL IA */\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 1024px) {\n .dash-grid { grid-template-columns: 1fr; }\n }\n @media (max-width: 768px) {\n .workspace-dash { padding: 90px 1rem 120px 1rem; }\n .dash-panel { padding: 1.5rem; }\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace-dash\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"dash-grid\">\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-purple); margin-bottom: 2rem;\">\n <div class=\"panel-title\"><span>📖 Propósito Fundacional</span> <div style=\"display:flex; gap:5px;\">${tagsHtml}</div></div>\n <div class=\"presentation-text\" id=\"pitchText\">${pitchText}</div>\n <button class=\"btn-open-paper\" id=\"btnOpenPaper\">📝 Desarrollar en Omni-Paper (Usenet)</button>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-blue); padding:0;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">🕸️ Topología del Ecosistema</div>\n <div class=\"mini-map-container\" id=\"dashMapContainer\">\n <div class=\"map-canvas map-svg-layer\" id=\"dashMapCanvas\">\n <svg id=\"dashMapSvg\">\n <defs>\n <marker id=\"arrow-tangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#00e676\"/></marker>\n <marker id=\"arrow-intangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#e040fb\"/></marker>\n </defs>\n <g id=\"dashMapPaths\"></g>\n </svg>\n </div>\n </div>\n <div style=\"padding: 0 2rem 2rem 2rem;\">\n <a href=\"/v8/map\" data-link style=\"color:var(--accent-blue); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Modificar Arquitectura &rarr;</a>\n </div>\n </div>\n </div>\n\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); margin-bottom: 2rem;\">\n <div class=\"panel-title\">🤖 Enjambre IA Activo</div>\n <div style=\"display:flex; margin-bottom:1rem;\">\n ${aiAvatarsHtml}\n </div>\n \n <div class=\"ai-arbitrage-panel\">\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Tokens Consumidos</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-blue);\">${(totalTokens / 1000).toFixed(1)}k</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Gasto API (USD)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">$${realApiCost.toFixed(4)}</span>\n </div>\n <div class=\"ai-stat-row\" style=\"border-top: 1px dashed #444; padding-top: 15px; margin-top: 5px;\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-green);\">Valor (Slices) IA</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-green);\">€${aiGrossValue.toLocaleString()}</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-purple);\">Ratio (REC)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-purple);\">${recRatio > 0 ? recRatio.toFixed(0) + 'x' : '0'}</span>\n </div>\n </div>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); padding:0; overflow:hidden;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">⚖️ Distribución de Equidad</div>\n <div id=\"dashLedgerContainer\" style=\"padding: 0 2rem 2rem 2rem;\"></div>\n <div style=\"background: rgba(0,230,118,0.1); padding: 15px 2rem; text-align:right;\">\n <a href=\"/v8/ledger\" data-link style=\"color:var(--accent-green); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Ir a la Notaría (Ledger Completo) &rarr;</a>\n </div>\n </div>\n\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem;\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.95rem; margin-bottom:2rem; line-height:1.5;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado. El <b>Dharma Coach</b> usa esto para reclutar talento.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem; text-align:center;\">\n <h2 style=\"color: var(--text-muted); margin-top:0;\">Módulo de Configuración de Red</h2>\n <p style=\"color:#888;\">Las opciones de gobernanza, APIs multimodales y Padrón de IAs se gestionan de forma segura y Zero-Trust desde la Consola Global.</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:2rem; text-decoration:none;\">Ir al Panteón Global</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none; background:var(--accent-purple); color:white; border:none; padding:10px 20px; border-radius:12px; font-weight:bold; cursor:pointer;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const project = state.projects.find(p => p.id === this.activeProjectId);\n if(!project) return;\n\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // 🔥 MAGIA DRY 1: RENDERIZAR MAPA EN EL DASHBOARD\n const dashMapCanvas = document.getElementById('dashMapCanvas');\n const dashMapPaths = document.getElementById('dashMapPaths');\n if (dashMapCanvas && dashMapPaths && project.roles) {\n const mr = new MapRenderer(dashMapCanvas, dashMapPaths, { \n isMacro: true, \n isHeatmap: false,\n markerSuffix: 'vis',\n trimSize: 20\n });\n mr.setData(project.roles, project.vna_flows || []);\n }\n\n // 🔥 MAGIA DRY 2: RENDERIZAR LEDGER (SOLO PIE Y CAP TABLE)\n const dashLedgerContainer = document.getElementById('dashLedgerContainer');\n if (dashLedgerContainer) {\n const lr = new LedgerRenderer(dashLedgerContainer, {\n projectId: project.id,\n showHistory: false // Ocultamos la tabla de Blockchain para no saturar el Dashboard\n });\n lr.render();\n }\n\n // 🔥 CREACIÓN DINÁMICA DE WORK ORDER (IR AL PAPER)\n const btnOpenPaper = document.getElementById('btnOpenPaper');\n if (btnOpenPaper) {\n btnOpenPaper.addEventListener('click', async () => {\n // Creamos una Work Order \"Teórica\" en el vuelo (Una Tarea Huérfana de investigación)\n const newHash = 'wo_research_' + Math.random().toString(36).substr(2, 9);\n \n await store.dispatch({\n type: 'SPAWN_WORK_ORDER',\n payload: {\n projectId: this.activeProjectId,\n workOrder: {\n hash: newHash, \n flowId: null, // No pertenece a ningún SOP, es investigación libre\n comentario: `Investigación y Desarrollo del Manifiesto / Prompt del proyecto.`,\n status: 'pinged', // Directamente en curso\n realHours: 0,\n sprintId: project.activeSprintId,\n soc_checklist: [], \n assigneeId: state.session.activeUserId // Asignada al PO\n }\n }\n });\n\n // Redirigimos al Omni-Paper con el Hash cargado\n window.location.href = `/v8/paper?hash=${newHash}`;\n });\n }\n\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Alias (@id):`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // Lógica Modal IA (Auditoría VNA & Equity)\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n const modalCloseBtn = document.getElementById('aiModalClose');\n\n if (modalCloseBtn) modalCloseBtn.onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n if (!modal) return;\n \n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en la Consola V14 antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Analizando el Ledger Fractal y la Topología...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = store.getState().globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Nodo: ${userName} | Equidad: ${((h.slices / totalSlices) * 100).toFixed(2)}%`;\n });\n }\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n roles: project.roles.map(r => r.name),\n cap_table_actual: capTableDetails\n };\n\n let systemPrompt = \"Eres un Master Architect de DAOs.\";\n if (type === 'audit') {\n systemPrompt += `\\nEvalúa si la topología y distribución de Slices refleja un ecosistema sano basado en la teoría Slicing Pie.`;\n } else {\n systemPrompt += `\\nRedacta un Pacto de Socios formal (Smart Contract / Legal Draft) basado en el Cap Table adjunto.`;\n }\n\n try {\n // LLamada Legacy API directa (Refactor inminente a Orchestrator en Sprints futuros)\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] }) });\n text = (await res.json()).candidates[0].content.parts[0].text;\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const a = document.createElement('a'); \n a.href = URL.createObjectURL(new Blob([text], {type:\"text/plain\"})); \n a.download = `${type}_${project.nombre.replace(/ /g,'_')}.txt`; \n a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "5797bdc13c92998eee06b5e2540f84e99add836a": {"t": 1785758900.990394, "n": 28325, "h": "8f3ddd0db790f0ef", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V8\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V8/V9 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience ? store.calculateResilience(project.id) : 100; // Protegido si el método no existe\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- NUEVO: CÁLCULO DE TELEMETRÍA (SPRINT 34.5) ---\n let aiGrossValue = 0; // Valor Slicing Pie minado por IA\n let realApiCost = 0; // Gasto en dólares real de los LLM\n let totalTokens = 0;\n\n // 1. Calculamos el Valor de Mercado minado por las IA\n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * (tx.horas || 1); \n }\n });\n\n // 2. Extraemos el coste real desde el array de telemetría inyectado por el Orquestador\n if (project.telemetry && project.telemetry.length > 0) {\n project.telemetry.forEach(log => {\n realApiCost += log.costInDollars || 0;\n totalTokens += (log.tokens?.total_tokens || log.tokens?.prompt_tokens + log.tokens?.completion_tokens) || 0;\n });\n }\n\n // Ratio de Eficiencia Cognitiva (REC)\n const recRatio = realApiCost > 0 ? (aiGrossValue / realApiCost) : 0;\n // ----------------------------------------------------\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Centro de Mando del Ecosistema\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n .workflow-schema { display: flex; justify-content: space-between; align-items: flex-start; padding: 1.5rem; margin-bottom: 2rem; }\n .schema-step { display: flex; flex-direction: column; align-items: center; text-decoration: none; text-align: center; gap: 12px; flex: 1; transition: all 0.3s; filter: grayscale(40%) opacity(0.8); }\n .schema-step:hover { transform: translateY(-5px); filter: grayscale(0%) opacity(1); }\n .s-icon { font-size: 2.5rem; background: rgba(255,255,255,0.03); width: 75px; height: 75px; display: flex; justify-content: center; align-items: center; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 20px rgba(0,0,0,0.4); transition: 0.3s; }\n .schema-step:hover .s-icon { border-color: var(--accent-blue); box-shadow: 0 15px 30px rgba(0,176,255,0.2); }\n .s-text { color: white; font-weight: 900; font-size: 0.95rem; text-transform: uppercase; }\n .s-text span { color: var(--accent-blue); font-size: 0.75rem; font-family: var(--font-mono); display: block; margin-top: 4px; font-weight: normal; }\n .schema-arrow { color: rgba(255,255,255,0.1); font-size: 2rem; font-weight: bold; margin-top: 20px; }\n\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; overflow: hidden; transition: max-height 0.4s ease; }\n .presentation-text.collapsed { max-height: 75px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }\n\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* 🔥 NUEVO PANEL DE TELEMETRÍA A2A */\n .ai-arbitrage-panel { background: linear-gradient(135deg, rgba(224, 64, 251, 0.05), rgba(0, 176, 255, 0.05)); border: 1px dashed var(--accent-purple); padding: 2rem; border-radius: 20px; margin-bottom: 2.5rem; display: flex; flex-direction:column; gap: 20px;}\n .ai-stat-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px;}\n .ai-stat-block { display: flex; flex-direction: column; gap: 5px; }\n .ai-stat-lbl { font-size: 0.75rem; color: #aaa; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }\n .ai-stat-val { font-size: 1.8rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 768px) {\n .workflow-schema { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; }\n .schema-arrow { display: none; }\n .s-icon { width: 60px; height: 60px; font-size: 2rem; border-radius: 16px; margin-bottom: 10px;}\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"glass-panel workflow-schema\">\n <a href=\"/v8/map\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🕸️</div>\n <div class=\"s-text\">1. Diseñar<br><span>(Mapa VNA)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/project\" data-link class=\"schema-step\">\n <div class=\"s-icon\">📋</div>\n <div class=\"s-text\">2. Asignar<br><span>(Kanban PULL)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/focus\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🍅</div>\n <div class=\"s-text\">3. Ejecutar<br><span>(Modo Focus)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/ledger\" data-link class=\"schema-step\">\n <div class=\"s-icon\">⚖️</div>\n <div class=\"s-text\">4. Sellar<br><span>(Ledger & Equity)</span></div>\n </a>\n </div>\n\n <div class=\"ai-arbitrage-panel\">\n <div class=\"ai-stat-row\">\n <div style=\"display:flex; align-items:center; gap: 15px;\">\n <div style=\"font-size: 2.5rem; filter: drop-shadow(0 0 10px rgba(224, 64, 251, 0.4));\">🤖</div>\n <div>\n <h3 style=\"color: var(--accent-purple); margin:0 0 5px 0; font-weight:900;\">Telemetría A2A (Arbitraje Cognitivo)</h3>\n <p style=\"color: #aaa; margin:0; font-size:0.85rem;\">Monitoreo de eficiencia (LLM Tokenomics) vs Valor Creado.</p>\n </div>\n </div>\n <div class=\"ai-stat-block\" style=\"text-align:right;\">\n <span class=\"ai-stat-lbl\">Tokens Consumidos</span>\n <span class=\"ai-stat-val\" style=\"font-size:1.2rem;\">${(totalTokens / 1000).toFixed(1)}k</span>\n </div>\n </div>\n \n <div class=\"ai-stat-row\" style=\"margin-top:10px; border-top:1px dashed rgba(255,255,255,0.1); padding-top:20px;\">\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\">Valor Slicing Pie Generado</span>\n <span class=\"ai-stat-val\" style=\"color:white;\">€${aiGrossValue.toLocaleString()}</span>\n </div>\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-red);\">Gasto Real API (USD)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">$${realApiCost.toFixed(4)}</span>\n </div>\n <div class=\"ai-stat-block\" style=\"padding-left: 1.5rem; border-left: 2px solid var(--accent-purple);\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-purple);\">Ratio (REC)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-purple);\">${recRatio > 0 ? recRatio.toFixed(0) + 'x' : '0'}</span>\n </div>\n </div>\n </div>\n\n <div class=\"glass-panel\" style=\"padding: 1.5rem 2rem; border-left: 4px solid var(--accent-blue); margin-bottom: 2.5rem;\">\n <div style=\"display:flex; gap: 10px; margin-bottom: 15px; flex-wrap:wrap;\">${tagsHtml}</div>\n <div class=\"presentation-text collapsed\" id=\"pitchText\">${pitchText.replace(/\\n/g, '<br>')}</div>\n <button id=\"btnTogglePitch\" style=\"background:none; border:none; color:var(--accent-blue); font-weight:900; cursor:pointer; padding:10px 0 0 0; font-size:0.8rem; text-transform:uppercase;\">Expandir Manifiesto ▾</button>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.3rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.85rem; margin-bottom:1.5rem; line-height:1.4;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: var(--text-muted);\">Módulo de Configuración de Red</h2>\n <p>Opciones de gobernanza y parámetros de la DAO. (Centralizado en la Consola Global V9).</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:1rem; text-decoration:none;\">Ir a Consola V9</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const hasAccess = store.canUserViewProject(this.activeProjectId, state.session.activeUserId, state.session.role);\n if (!hasAccess) return;\n\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n const pitchEl = document.getElementById('pitchText');\n const btnToggle = document.getElementById('btnTogglePitch');\n if (pitchEl && btnToggle) {\n btnToggle.addEventListener('click', () => {\n const isCollapsed = pitchEl.classList.contains('collapsed');\n pitchEl.classList.toggle('collapsed');\n btnToggle.innerText = isCollapsed ? 'Contraer Manifiesto ▴' : 'Expandir Manifiesto ▾';\n });\n }\n\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Alias (@id):`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n const modalCloseBtn = document.getElementById('aiModalClose');\n\n if (modalCloseBtn) modalCloseBtn.onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n if (!modal) return;\n const project = store.getState().projects.find(p => p.id === this.activeProjectId);\n \n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en la Consola V9 antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Analizando el Ledger Fractal...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = store.getState().globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Nodo: ${userName} | Equidad: ${((h.slices / totalSlices) * 100).toFixed(2)}%`;\n });\n }\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n cap_table_actual: capTableDetails\n };\n\n let systemPrompt = \"Eres un Master Architect de DAOs.\";\n if (type === 'audit') {\n systemPrompt += `\\nEvalúa si la distribución de Slices refleja un ecosistema sano basado en la teoría Slicing Pie.`;\n } else {\n systemPrompt += `\\nRedacta un Pacto de Socios formal basado en el Cap Table adjunto.`;\n }\n\n try {\n // 🔥 NOTA PARA EL SPRINT 34.5: \n // En el siguiente refactor, moveremos esto al Orchestrator.js para no violar el DRY.\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] }) });\n text = (await res.json()).candidates[0].content.parts[0].text;\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const a = document.createElement('a'); \n a.href = URL.createObjectURL(new Blob([text], {type:\"text/plain\"})); \n a.download = `${type}_${project.nombre.replace(/ /g,'_')}.txt`; \n a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "c15b0639fb9f74528fb7ce11c379e301bacd4db7": {"t": 1785758902.46959, "n": 29772, "h": "bb8641a5f7ff334d", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V8\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V8 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience(project.id);\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- CÁLCULO DE ARBITRAJE IA (Fase 17) ---\n let aiGrossValue = 0;\n let aiCost = 0;\n \n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n const hrs = tx.horas || 1;\n // Valor de Mercado que hubiera cobrado un humano\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * hrs; \n // Coste real de la API\n aiCost += (user.profile.apiCostPerHour || 0.15) * hrs;\n }\n });\n const aiNetSavings = aiGrossValue - aiCost;\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n // --- HEADER V8 CONFIG ---\n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Centro de Mando del Ecosistema\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n .workflow-schema { display: flex; justify-content: space-between; align-items: flex-start; padding: 1.5rem; margin-bottom: 2rem; }\n .schema-step { display: flex; flex-direction: column; align-items: center; text-decoration: none; text-align: center; gap: 12px; flex: 1; transition: all 0.3s; filter: grayscale(40%) opacity(0.8); }\n .schema-step:hover { transform: translateY(-5px); filter: grayscale(0%) opacity(1); }\n .s-icon { font-size: 2.5rem; background: rgba(255,255,255,0.03); width: 75px; height: 75px; display: flex; justify-content: center; align-items: center; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 20px rgba(0,0,0,0.4); transition: 0.3s; }\n .schema-step:hover .s-icon { border-color: var(--accent-blue); box-shadow: 0 15px 30px rgba(0,176,255,0.2); }\n .s-text { color: white; font-weight: 900; font-size: 0.95rem; text-transform: uppercase; }\n .s-text span { color: var(--accent-blue); font-size: 0.75rem; font-family: var(--font-mono); display: block; margin-top: 4px; font-weight: normal; }\n .schema-arrow { color: rgba(255,255,255,0.1); font-size: 2rem; font-weight: bold; margin-top: 20px; }\n\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; overflow: hidden; transition: max-height 0.4s ease; }\n .presentation-text.collapsed { max-height: 75px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }\n\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* AI ARBITRAGE PANEL */\n .ai-arbitrage-panel { background: linear-gradient(135deg, rgba(224, 64, 251, 0.05), rgba(0, 176, 255, 0.05)); border: 1px dashed var(--accent-purple); padding: 2rem; border-radius: 20px; margin-bottom: 2.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px;}\n .ai-stat-block { display: flex; flex-direction: column; gap: 5px; }\n .ai-stat-lbl { font-size: 0.75rem; color: #aaa; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }\n .ai-stat-val { font-size: 1.8rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 768px) {\n .workflow-schema { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; }\n .schema-arrow { display: none; }\n .s-icon { width: 60px; height: 60px; font-size: 2rem; border-radius: 16px; margin-bottom: 10px;}\n .ai-arbitrage-panel { flex-direction: column; align-items: flex-start; }\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"glass-panel workflow-schema\">\n <a href=\"/v8/map\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🕸️</div>\n <div class=\"s-text\">1. Diseñar<br><span>(Mapa VNA)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/project\" data-link class=\"schema-step\">\n <div class=\"s-icon\">📋</div>\n <div class=\"s-text\">2. Asignar<br><span>(Kanban)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/focus\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🍅</div>\n <div class=\"s-text\">3. Ejecutar<br><span>(Focus Mode)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/ledger\" data-link class=\"schema-step\">\n <div class=\"s-icon\">⚖️</div>\n <div class=\"s-text\">4. Cobrar<br><span>(Ledger Equity)</span></div>\n </a>\n </div>\n\n <div class=\"ai-arbitrage-panel\">\n <div style=\"display:flex; align-items:center; gap: 15px;\">\n <div style=\"font-size: 2.5rem; filter: drop-shadow(0 0 10px rgba(224, 64, 251, 0.4));\">🤖</div>\n <div>\n <h3 style=\"color: var(--accent-purple); margin:0 0 5px 0; font-weight:900;\">Arbitraje Cognitivo (IA)</h3>\n <p style=\"color: #aaa; margin:0; font-size:0.85rem;\">Diferencial entre el valor minado por IAs y su coste API real.</p>\n </div>\n </div>\n <div style=\"display:flex; gap: 2rem; flex-wrap:wrap;\">\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\">Valor Bruto Facturado</span>\n <span class=\"ai-stat-val\">€${aiGrossValue.toFixed(2)}</span>\n </div>\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-red);\">Coste API Asumido</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">- €${aiCost.toFixed(2)}</span>\n </div>\n <div class=\"ai-stat-block\" style=\"padding-left: 1rem; border-left: 1px dashed rgba(255,255,255,0.2);\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-green);\">Ahorro Neto (Profit)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-green);\">€${aiNetSavings.toFixed(2)}</span>\n </div>\n </div>\n </div>\n\n <div class=\"glass-panel\" style=\"padding: 1.5rem 2rem; border-left: 4px solid var(--accent-blue); margin-bottom: 2.5rem;\">\n <div style=\"display:flex; gap: 10px; margin-bottom: 15px; flex-wrap:wrap;\">${tagsHtml}</div>\n <div class=\"presentation-text collapsed\" id=\"pitchText\">${pitchText.replace(/\\n/g, '<br>')}</div>\n <button id=\"btnTogglePitch\" style=\"background:none; border:none; color:var(--accent-blue); font-weight:900; cursor:pointer; padding:10px 0 0 0; font-size:0.8rem; text-transform:uppercase;\">Expandir Manifiesto ▾</button>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.3rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.85rem; margin-bottom:1.5rem; line-height:1.4;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: var(--text-muted);\">Módulo de Configuración de Red</h2>\n <p>Opciones de gobernanza y parámetros de la DAO. (Centralizado en la Consola Global V8).</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:1rem; text-decoration:none;\">Ir a Settings Global</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n \n // --- PREVENCIÓN DE ERROR: Si el usuario no tiene acceso, abortamos los listeners ---\n const hasAccess = store.canUserViewProject(this.activeProjectId, state.session.activeUserId, state.session.role);\n if (!hasAccess) return;\n\n const project = state.projects.find(p => p.id === this.activeProjectId);\n\n // -- LÓGICA DE TABS V8 --\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // -- LÓGICA ACORDEÓN PITCH --\n const pitchEl = document.getElementById('pitchText');\n const btnToggle = document.getElementById('btnTogglePitch');\n if (pitchEl && btnToggle) {\n btnToggle.addEventListener('click', () => {\n const isCollapsed = pitchEl.classList.contains('collapsed');\n pitchEl.classList.toggle('collapsed');\n btnToggle.innerText = isCollapsed ? 'Contraer Manifiesto ▴' : 'Expandir Manifiesto ▾';\n });\n }\n\n // -- INVITACIONES --\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Wallet:`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // -- MÓDULO IA CONECTADO AL MAGIC BUTTON --\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n const modalCloseBtn = document.getElementById('aiModalClose');\n\n if (modalCloseBtn) {\n modalCloseBtn.onclick = () => modal.style.display = 'none';\n }\n\n const runAI = async (type) => {\n if (!modal) return;\n \n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en Configuración Global antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Leyendo el Ledger Inmutable...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n // EXTRACCIÓN PROFUNDA\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío. Aún no se ha minado Equity.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = state.globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Socio: ${userName} | Participación: ${((h.slices / totalSlices) * 100).toFixed(2)}% | Capital: ${Number(h.slices).toFixed(2)} Slices`;\n });\n }\n\n const realLedger = (project.ledger || []).map(l => {\n const u = state.globalUsers?.find(gu => gu.id === l.userId);\n const userName = u ? u.name : (l.userId || 'Sistema');\n const isCap = l.isCapital || l.roleId === 'capital' || String(l.entregable || l.description).includes('[Capital');\n \n if (isCap) {\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} inyectó CAPITAL. Slices: +${l.valorCongelado}.`;\n } else {\n const role = project.roles?.find(r => r.id === l.roleId);\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} ejecutó Trabajo (${role ? role.name : 'Operativo'}) ${l.horas}h. Slices: +${l.valorCongelado}.`;\n }\n });\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n cap_table_actual: capTableDetails,\n registro_aportaciones_reales: realLedger.length > 0 ? realLedger : [\"Sin transacciones.\"]\n };\n\n let systemPrompt = state.config?.globalPrompt || \"Eres un Master Architect de DAOs.\";\n \n if (type === 'audit') {\n systemPrompt += `\\nMisión: Eres un Auditor VNA y experto en economía Slicing Pie. Identifica nodos inversores vs operativos. Evalúa si hay un buen equilibrio o cuellos de botella. Formato texto plano legible.`;\n } else {\n systemPrompt += `\\nMisión: Eres un Abogado Notarial Web3 experto en Slicing Pie. Redacta un \"Pacto de Socios\" formal basado estrictamente en los datos recibidos (Cap Table y Aportaciones).`;\n }\n\n try {\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API DeepSeek\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API OpenAI\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] })\n });\n if (!res.ok) throw new Error(\"Error en API Gemini\");\n text = (await res.json()).candidates[0].content.parts[0].text;\n } else if (provider === 'custom') {\n const customUrl = localStorage.getItem('tt_ai_custom_url') || 'http://localhost:1234/v1/chat/completions';\n const res = await fetch(customUrl, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ prompt: systemPrompt, data: dataPayload })\n });\n if (!res.ok) throw new Error(\"Error en Endpoint Custom\");\n const data = await res.json();\n text = typeof data === 'string' ? data : JSON.stringify(data);\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n \n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const b = new Blob([text], {type:\"text/plain\"});\n const u = URL.createObjectURL(b);\n const a = document.createElement('a'); a.href=u; a.download=`${type}_${project.nombre.replace(/ /g,'_')}.txt`; a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n // --- CONEXIÓN DEL EVENTO MAGIC BUTTON ---\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "87358a7305de08df086d095533959bc79a2cdb6a": {"t": 1785758904.0326028, "n": 29380, "h": "8b5498cb112e8930", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V8\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V8 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience(project.id);\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- CÁLCULO DE ARBITRAJE IA (Fase 17) ---\n let aiGrossValue = 0;\n let aiCost = 0;\n \n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n const hrs = tx.horas || 1;\n // Valor de Mercado que hubiera cobrado un humano\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * hrs; \n // Coste real de la API\n aiCost += (user.profile.apiCostPerHour || 0.15) * hrs;\n }\n });\n const aiNetSavings = aiGrossValue - aiCost;\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n // --- HEADER V8 CONFIG ---\n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Centro de Mando del Ecosistema\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n .workflow-schema { display: flex; justify-content: space-between; align-items: flex-start; padding: 1.5rem; margin-bottom: 2rem; }\n .schema-step { display: flex; flex-direction: column; align-items: center; text-decoration: none; text-align: center; gap: 12px; flex: 1; transition: all 0.3s; filter: grayscale(40%) opacity(0.8); }\n .schema-step:hover { transform: translateY(-5px); filter: grayscale(0%) opacity(1); }\n .s-icon { font-size: 2.5rem; background: rgba(255,255,255,0.03); width: 75px; height: 75px; display: flex; justify-content: center; align-items: center; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 20px rgba(0,0,0,0.4); transition: 0.3s; }\n .schema-step:hover .s-icon { border-color: var(--accent-blue); box-shadow: 0 15px 30px rgba(0,176,255,0.2); }\n .s-text { color: white; font-weight: 900; font-size: 0.95rem; text-transform: uppercase; }\n .s-text span { color: var(--accent-blue); font-size: 0.75rem; font-family: var(--font-mono); display: block; margin-top: 4px; font-weight: normal; }\n .schema-arrow { color: rgba(255,255,255,0.1); font-size: 2rem; font-weight: bold; margin-top: 20px; }\n\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; overflow: hidden; transition: max-height 0.4s ease; }\n .presentation-text.collapsed { max-height: 75px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }\n\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* AI ARBITRAGE PANEL */\n .ai-arbitrage-panel { background: linear-gradient(135deg, rgba(224, 64, 251, 0.05), rgba(0, 176, 255, 0.05)); border: 1px dashed var(--accent-purple); padding: 2rem; border-radius: 20px; margin-bottom: 2.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px;}\n .ai-stat-block { display: flex; flex-direction: column; gap: 5px; }\n .ai-stat-lbl { font-size: 0.75rem; color: #aaa; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; }\n .ai-stat-val { font-size: 1.8rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 768px) {\n .workflow-schema { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; }\n .schema-arrow { display: none; }\n .s-icon { width: 60px; height: 60px; font-size: 2rem; border-radius: 16px; margin-bottom: 10px;}\n .ai-arbitrage-panel { flex-direction: column; align-items: flex-start; }\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"glass-panel workflow-schema\">\n <a href=\"/v8/map\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🕸️</div>\n <div class=\"s-text\">1. Diseñar<br><span>(Mapa VNA)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/project\" data-link class=\"schema-step\">\n <div class=\"s-icon\">📋</div>\n <div class=\"s-text\">2. Asignar<br><span>(Kanban)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/focus\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🍅</div>\n <div class=\"s-text\">3. Ejecutar<br><span>(Focus Mode)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/ledger\" data-link class=\"schema-step\">\n <div class=\"s-icon\">⚖️</div>\n <div class=\"s-text\">4. Cobrar<br><span>(Ledger Equity)</span></div>\n </a>\n </div>\n\n <div class=\"ai-arbitrage-panel\">\n <div style=\"display:flex; align-items:center; gap: 15px;\">\n <div style=\"font-size: 2.5rem; filter: drop-shadow(0 0 10px rgba(224, 64, 251, 0.4));\">🤖</div>\n <div>\n <h3 style=\"color: var(--accent-purple); margin:0 0 5px 0; font-weight:900;\">Arbitraje Cognitivo (IA)</h3>\n <p style=\"color: #aaa; margin:0; font-size:0.85rem;\">Diferencial entre el valor minado por IAs y su coste API real.</p>\n </div>\n </div>\n <div style=\"display:flex; gap: 2rem; flex-wrap:wrap;\">\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\">Valor Bruto Facturado</span>\n <span class=\"ai-stat-val\">€${aiGrossValue.toFixed(2)}</span>\n </div>\n <div class=\"ai-stat-block\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-red);\">Coste API Asumido</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">- €${aiCost.toFixed(2)}</span>\n </div>\n <div class=\"ai-stat-block\" style=\"padding-left: 1rem; border-left: 1px dashed rgba(255,255,255,0.2);\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-green);\">Ahorro Neto (Profit)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-green);\">€${aiNetSavings.toFixed(2)}</span>\n </div>\n </div>\n </div>\n\n <div class=\"glass-panel\" style=\"padding: 1.5rem 2rem; border-left: 4px solid var(--accent-blue); margin-bottom: 2.5rem;\">\n <div style=\"display:flex; gap: 10px; margin-bottom: 15px; flex-wrap:wrap;\">${tagsHtml}</div>\n <div class=\"presentation-text collapsed\" id=\"pitchText\">${pitchText.replace(/\\n/g, '<br>')}</div>\n <button id=\"btnTogglePitch\" style=\"background:none; border:none; color:var(--accent-blue); font-weight:900; cursor:pointer; padding:10px 0 0 0; font-size:0.8rem; text-transform:uppercase;\">Expandir Manifiesto ▾</button>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.3rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.85rem; margin-bottom:1.5rem; line-height:1.4;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: var(--text-muted);\">Módulo de Configuración de Red</h2>\n <p>Opciones de gobernanza y parámetros de la DAO. (Centralizado en la Consola Global V8).</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:1rem; text-decoration:none;\">Ir a Settings Global</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const project = state.projects.find(p => p.id === this.activeProjectId);\n\n // -- LÓGICA DE TABS V8 --\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // -- LÓGICA ACORDEÓN PITCH --\n const pitchEl = document.getElementById('pitchText');\n const btnToggle = document.getElementById('btnTogglePitch');\n if (pitchEl && btnToggle) {\n btnToggle.addEventListener('click', () => {\n const isCollapsed = pitchEl.classList.contains('collapsed');\n pitchEl.classList.toggle('collapsed');\n btnToggle.innerText = isCollapsed ? 'Contraer Manifiesto ▴' : 'Expandir Manifiesto ▾';\n });\n }\n\n // -- INVITACIONES --\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Wallet:`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // -- MÓDULO IA CONECTADO AL MAGIC BUTTON --\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n\n document.getElementById('aiModalClose').onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en Configuración Global antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Leyendo el Ledger Inmutable...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n // EXTRACCIÓN PROFUNDA\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío. Aún no se ha minado Equity.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = state.globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Socio: ${userName} | Participación: ${((h.slices / totalSlices) * 100).toFixed(2)}% | Capital: ${Number(h.slices).toFixed(2)} Slices`;\n });\n }\n\n const realLedger = (project.ledger || []).map(l => {\n const u = state.globalUsers?.find(gu => gu.id === l.userId);\n const userName = u ? u.name : (l.userId || 'Sistema');\n const isCap = l.isCapital || l.roleId === 'capital' || String(l.entregable || l.description).includes('[Capital');\n \n if (isCap) {\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} inyectó CAPITAL. Slices: +${l.valorCongelado}.`;\n } else {\n const role = project.roles?.find(r => r.id === l.roleId);\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} ejecutó Trabajo (${role ? role.name : 'Operativo'}) ${l.horas}h. Slices: +${l.valorCongelado}.`;\n }\n });\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n cap_table_actual: capTableDetails,\n registro_aportaciones_reales: realLedger.length > 0 ? realLedger : [\"Sin transacciones.\"]\n };\n\n let systemPrompt = state.config?.globalPrompt || \"Eres un Master Architect de DAOs.\";\n \n if (type === 'audit') {\n systemPrompt += `\\nMisión: Eres un Auditor VNA y experto en economía Slicing Pie. Identifica nodos inversores vs operativos. Evalúa si hay un buen equilibrio o cuellos de botella. Formato texto plano legible.`;\n } else {\n systemPrompt += `\\nMisión: Eres un Abogado Notarial Web3 experto en Slicing Pie. Redacta un \"Pacto de Socios\" formal basado estrictamente en los datos recibidos (Cap Table y Aportaciones).`;\n }\n\n try {\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API DeepSeek\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API OpenAI\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] })\n });\n if (!res.ok) throw new Error(\"Error en API Gemini\");\n text = (await res.json()).candidates[0].content.parts[0].text;\n } else if (provider === 'custom') {\n const customUrl = localStorage.getItem('tt_ai_custom_url') || 'http://localhost:1234/v1/chat/completions';\n const res = await fetch(customUrl, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ prompt: systemPrompt, data: dataPayload })\n });\n if (!res.ok) throw new Error(\"Error en Endpoint Custom\");\n const data = await res.json();\n text = typeof data === 'string' ? data : JSON.stringify(data);\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n \n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const b = new Blob([text], {type:\"text/plain\"});\n const u = URL.createObjectURL(b);\n const a = document.createElement('a'); a.href=u; a.download=`${type}_${project.nombre.replace(/ /g,'_')}.txt`; a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n // --- CONEXIÓN DEL EVENTO MAGIC BUTTON ---\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "c359d05613922de65d1cda27645f01cf3330e290": {"t": 1785758907.9053335, "n": 25962, "h": "40badcfc244475e1", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V8\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n // Recuperar proyecto activo\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V8 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience(project.id);\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n // --- HEADER V8 CONFIG (TABS + MAGIC BUTTON) ---\n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, // El Header lo pintará en el Badge Verde automáticamente\n tagline: \"Centro de Mando del Ecosistema\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n /* ANIMACIONES Y MICRO-INTERACCIONES V8 */\n .workflow-schema { display: flex; justify-content: space-between; align-items: flex-start; padding: 1.5rem; margin-bottom: 2rem; }\n .schema-step { display: flex; flex-direction: column; align-items: center; text-decoration: none; text-align: center; gap: 12px; flex: 1; transition: all 0.3s; filter: grayscale(40%) opacity(0.8); }\n .schema-step:hover { transform: translateY(-5px); filter: grayscale(0%) opacity(1); }\n .s-icon { font-size: 2.5rem; background: rgba(255,255,255,0.03); width: 75px; height: 75px; display: flex; justify-content: center; align-items: center; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 20px rgba(0,0,0,0.4); transition: 0.3s; }\n .schema-step:hover .s-icon { border-color: var(--accent-blue); box-shadow: 0 15px 30px rgba(0,176,255,0.2); }\n .s-text { color: white; font-weight: 900; font-size: 0.95rem; text-transform: uppercase; }\n .s-text span { color: var(--accent-blue); font-size: 0.75rem; font-family: var(--font-mono); display: block; margin-top: 4px; font-weight: normal; }\n .schema-arrow { color: rgba(255,255,255,0.1); font-size: 2rem; font-weight: bold; margin-top: 20px; }\n\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; overflow: hidden; transition: max-height 0.4s ease; }\n .presentation-text.collapsed { max-height: 75px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }\n\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 768px) {\n .workflow-schema { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem 1.5rem; }\n .schema-arrow { display: none; }\n .s-icon { width: 60px; height: 60px; font-size: 2rem; border-radius: 16px; margin-bottom: 10px;}\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n <div class=\"glass-panel workflow-schema\">\n <a href=\"/v8/map\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🕸️</div>\n <div class=\"s-text\">1. Diseñar<br><span>(Mapa VNA)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/project\" data-link class=\"schema-step\">\n <div class=\"s-icon\">📋</div>\n <div class=\"s-text\">2. Asignar<br><span>(Kanban)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/focus\" data-link class=\"schema-step\">\n <div class=\"s-icon\">🍅</div>\n <div class=\"s-text\">3. Ejecutar<br><span>(Focus Mode)</span></div>\n </a>\n <div class=\"schema-arrow\">→</div>\n <a href=\"/v8/ledger\" data-link class=\"schema-step\">\n <div class=\"s-icon\">⚖️</div>\n <div class=\"s-text\">4. Cobrar<br><span>(Ledger Equity)</span></div>\n </a>\n </div>\n\n <div class=\"glass-panel\" style=\"padding: 1.5rem 2rem; border-left: 4px solid var(--accent-purple);\">\n <div style=\"display:flex; gap: 10px; margin-bottom: 15px; flex-wrap:wrap;\">${tagsHtml}</div>\n <div class=\"presentation-text collapsed\" id=\"pitchText\">${pitchText.replace(/\\n/g, '<br>')}</div>\n <button id=\"btnTogglePitch\" style=\"background:none; border:none; color:var(--accent-purple); font-weight:900; cursor:pointer; padding:10px 0 0 0; font-size:0.8rem; text-transform:uppercase;\">Expandir Manifiesto ▾</button>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos Humanos</span>\n </div>\n </section>\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.3rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.85rem; margin-bottom:1.5rem; line-height:1.4;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano asignado.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\">\n <h2 style=\"color: var(--text-muted);\">Módulo de Configuración de Red</h2>\n <p>Opciones de gobernanza y parámetros de la DAO (En desarrollo Sprint 4).</p>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const project = state.projects.find(p => p.id === this.activeProjectId);\n\n // -- LÓGICA DE TABS V8 --\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // -- LÓGICA ACORDEÓN PITCH --\n const pitchEl = document.getElementById('pitchText');\n const btnToggle = document.getElementById('btnTogglePitch');\n if (pitchEl && btnToggle) {\n btnToggle.addEventListener('click', () => {\n const isCollapsed = pitchEl.classList.contains('collapsed');\n pitchEl.classList.toggle('collapsed');\n btnToggle.innerText = isCollapsed ? 'Contraer Manifiesto ▴' : 'Expandir Manifiesto ▾';\n });\n }\n\n // -- INVITACIONES --\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario para el rol [${roleName}]. Introduce su email o Wallet:`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // -- MÓDULO IA CONECTADO AL MAGIC BUTTON --\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n\n document.getElementById('aiModalClose').onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en Configuración Global antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Leyendo el Ledger Inmutable...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n // EXTRACCIÓN PROFUNDA\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío. Aún no se ha minado Equity.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = state.globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Socio: ${userName} | Participación: ${((h.slices / totalSlices) * 100).toFixed(2)}% | Capital: ${Number(h.slices).toFixed(2)} Slices`;\n });\n }\n\n const realLedger = (project.ledger || []).map(l => {\n const u = state.globalUsers?.find(gu => gu.id === l.userId);\n const userName = u ? u.name : (l.userId || 'Sistema');\n const isCap = l.isCapital || l.roleId === 'capital' || String(l.entregable || l.description).includes('[Capital');\n \n if (isCap) {\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} inyectó CAPITAL. Slices: +${l.valorCongelado}.`;\n } else {\n const role = project.roles?.find(r => r.id === l.roleId);\n return `[${new Date(l.timestamp).toLocaleDateString()}] ${userName} ejecutó Trabajo (${role ? role.name : 'Operativo'}) ${l.horas}h. Slices: +${l.valorCongelado}.`;\n }\n });\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n cap_table_actual: capTableDetails,\n registro_aportaciones_reales: realLedger.length > 0 ? realLedger : [\"Sin transacciones.\"]\n };\n\n let systemPrompt = state.config?.globalPrompt || \"Eres un Master Architect de DAOs.\";\n \n if (type === 'audit') {\n systemPrompt += `\\nMisión: Eres un Auditor VNA y experto en economía Slicing Pie. Identifica nodos inversores vs operativos. Evalúa si hay un buen equilibrio o cuellos de botella. Formato texto plano legible.`;\n } else {\n systemPrompt += `\\nMisión: Eres un Abogado Notarial Web3 experto en Slicing Pie. Redacta un \"Pacto de Socios\" formal basado estrictamente en los datos recibidos (Cap Table y Aportaciones).`;\n }\n\n try {\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API DeepSeek\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] })\n });\n if (!res.ok) throw new Error(\"Error en API OpenAI\");\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] })\n });\n if (!res.ok) throw new Error(\"Error en API Gemini\");\n text = (await res.json()).candidates[0].content.parts[0].text;\n } else if (provider === 'custom') {\n const customUrl = localStorage.getItem('tt_ai_custom_url') || 'http://localhost:1234/v1/chat/completions';\n const res = await fetch(customUrl, {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n body: JSON.stringify({ prompt: systemPrompt, data: dataPayload })\n });\n if (!res.ok) throw new Error(\"Error en Endpoint Custom\");\n const data = await res.json();\n text = typeof data === 'string' ? data : JSON.stringify(data);\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n \n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const b = new Blob([text], {type:\"text/plain\"});\n const u = URL.createObjectURL(b);\n const a = document.createElement('a'); a.href=u; a.download=`${type}_${project.nombre.replace(/ /g,'_')}.txt`; a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n // --- CONEXIÓN DEL EVENTO MAGIC BUTTON ---\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}, "b86427a2a9ef6edb8888bc2cda2bb9b27653eaa5": {"t": 1786067546.5538695, "n": 33099, "h": "1bc69e8b79dda86a", "c": "// v8/js/views/DashboardView.js\nimport { store } from '../core/store.js';\nimport { KB } from '../core/kb.js'; // Importamos KB para la integración RAG si fuera necesaria\nimport { Sidebar } from '../components/Sidebar.js';\nimport { PageHeader } from '../components/PageHeader.js';\nimport { MapRenderer } from '../components/MapRenderer.js'; \nimport { LedgerRenderer } from '../components/LedgerRenderer.js'; \n\nexport default class DashboardView {\n constructor() {\n document.title = \"Dashboard de Red | TeamTowers V14\";\n this.activeProjectId = null;\n this.currentTab = 'overview';\n }\n\n async getHtml() {\n const state = store.getState();\n const activeUserId = state.session.activeUserId;\n const globalRole = state.session.role;\n\n let project = state.projects.find(p => p.id === localStorage.getItem('tt_active_project'));\n if (!project && state.projects.length > 0) {\n project = state.projects[state.projects.length - 1];\n }\n\n if (!project) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; padding: 4rem; max-width: 500px;\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1; filter: drop-shadow(0 0 20px rgba(0,176,255,0.3));\">🛰️</div>\n <h2 style=\"color:white; margin-top:0; font-weight:900; font-size:2rem;\">Radar Vacío</h2>\n <p style=\"color:var(--text-muted); margin-bottom: 2.5rem; font-size:1.1rem;\">Aún no has instanciado ninguna red en este Kernel.</p>\n <a href=\"/v8/create\" data-link class=\"btn-primary\" style=\"text-decoration:none;\">➕ Instanciar Proyecto</a>\n </div>\n </main>\n </div>\n `;\n }\n\n this.activeProjectId = project.id;\n const hasAccess = store.canUserViewProject(project.id, activeUserId, globalRole);\n \n if (!hasAccess) {\n return `\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n <main class=\"workspace\" style=\"justify-content:center; align-items:center; display:flex;\">\n <div class=\"glass-panel\" style=\"text-align:center; border: 1px dashed var(--accent-red); padding: 4rem; max-width: 600px; background: rgba(255, 82, 82, 0.05);\">\n <div style=\"font-size: 5rem; margin-bottom: 1.5rem; line-height:1;\">🔒</div>\n <h1 style=\"color: var(--accent-red); margin-top:0; font-weight:900; letter-spacing:-1px;\">ACCESO DENEGADO</h1>\n <p style=\"color: #ccc; font-size:1.1rem;\">Este Ecosistema es privado. No eres un nodo reconocido en su topología.</p>\n </div>\n </main>\n </div>\n `;\n }\n\n // --- CÁLCULOS CORE V14 ---\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n const totalHours = (project.ledger || []).reduce((sum, l) => sum + (l.horas || 0), 0);\n const resilience = store.calculateResilience ? store.calculateResilience(project.id) : 100;\n \n const rolesActivos = project.roles.filter(r => !r.isArchived);\n const asignaciones = project.asignaciones || [];\n const sillasVacias = rolesActivos.filter(r => !asignaciones.find(a => a.roleId === r.id));\n\n // --- CÁLCULO DE TELEMETRÍA (Arbitraje IA) ---\n let aiGrossValue = 0; \n let realApiCost = 0; \n let totalTokens = 0;\n\n (project.ledger || []).forEach(tx => {\n const user = state.globalUsers.find(u => u.id === tx.userId);\n if (user && user.profile?.isAi) {\n aiGrossValue += (tx.fmv || 50) * (tx.multiplier || 1) * (tx.horas || 1); \n }\n });\n\n if (project.telemetry && project.telemetry.length > 0) {\n project.telemetry.forEach(log => {\n realApiCost += log.costInDollars || 0;\n totalTokens += (log.tokens?.total_tokens || log.tokens?.prompt_tokens + log.tokens?.completion_tokens) || 0;\n });\n }\n const recRatio = realApiCost > 0 ? (aiGrossValue / realApiCost) : 0;\n \n // --- IDENTIFICACIÓN DEL ENJAMBRE IA ASIGNADO ---\n const projectUsers = project.usuarios || [];\n const aisInProject = projectUsers.map(u => state.globalUsers.find(gu => gu.id === u.id)).filter(u => u && u.profile?.isAi);\n const aiAvatarsHtml = aisInProject.length > 0 \n ? aisInProject.map(ai => `<div class=\"ai-avatar-badge\" title=\"${ai.name} (${ai.id})\">🤖</div>`).join('')\n : `<div style=\"color:#888; font-size:0.8rem; font-style:italic;\">No hay Agentes IA en la Colla.</div>`;\n\n let vacantesHtml = sillasVacias.length === 0 \n ? `<div style=\"padding: 1.5rem; background: rgba(0, 230, 118, 0.05); border: 1px solid rgba(0, 230, 118, 0.2); border-radius: 12px; color: var(--accent-green); text-align: center; font-weight: bold;\">✅ Arquitectura completa. Sin vacantes estructurales.</div>`\n : sillasVacias.map(r => `\n <div style=\"display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.4); border: 1px solid var(--glass-border); padding: 1.2rem; border-radius: 12px; margin-bottom: 12px; transition: 0.3s;\" onmouseover=\"this.style.borderColor='var(--accent-blue)'\" onmouseout=\"this.style.borderColor='var(--glass-border)'\">\n <div style=\"display:flex; align-items:center; gap:15px;\">\n <div style=\"font-size:1.8rem; line-height:1; filter:drop-shadow(0 0 5px rgba(255,255,255,0.2));\">${this.getIcon(r.levelId)}</div>\n <div>\n <div style=\"color: white; font-weight: 900; font-size: 1rem;\">${r.name}</div>\n <div style=\"font-size: 0.8rem; color: #888; font-family: var(--font-mono); margin-top: 4px;\">${r.levelId} | FMV: <span style=\"color:var(--accent-green); font-weight:bold;\">${r.fmv}€/h</span></div>\n </div>\n </div>\n <button class=\"btn-invite\" data-rolename=\"${r.name}\" style=\"background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; font-weight: 900; transition: all 0.2s; text-transform: uppercase;\">➕ Invitar</button>\n </div>\n `).join('');\n\n const pitchText = project.presentation || project.prompt || 'El propósito fundacional de esta red está en fase de definición...';\n const tagsHtml = (project.tags && project.tags.length > 0) \n ? project.tags.map(t => `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#${t}</span>`).join('') \n : `<span style=\"font-family: var(--font-mono); font-size: 0.7rem; color: #888; border: 1px solid #333; padding: 4px 10px; border-radius: 6px; background: rgba(0,0,0,0.5);\">#VNA</span>`;\n \n const headerConfig = {\n title: project.nombre,\n subtitle: project.archetype, \n tagline: \"Ojo del Castell (Centro de Mando)\",\n tabs: [\n { id: 'overview', label: '📊 Resumen Operativo', active: this.currentTab === 'overview' },\n { id: 'market', label: '🎯 Mercado Interno', active: this.currentTab === 'market', badge: sillasVacias.length || null },\n { id: 'settings', label: '⚙️ Configuración', active: this.currentTab === 'settings' }\n ],\n magicActions: [\n { id: 'audit', label: 'Auditoría VNA & Equity', icon: '🧠', isAi: true, tokens: 150 },\n { id: 'legal', label: 'Emitir Pacto Socios', icon: '⚖️', isAi: true, tokens: 300 }\n ]\n };\n\n return `\n <style>\n ${MapRenderer.getStyles()}\n ${LedgerRenderer.getStyles()}\n\n .app-layout { display: flex; height: 100vh; overflow: hidden; background: var(--bg-dark); font-family: var(--font-main); width: 100%;}\n .workspace-dash { flex: 1; padding: 2rem 3rem; overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth; box-sizing: border-box;}\n \n .tab-content { display: none; animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); padding-bottom: 5rem; width: 100%; box-sizing: border-box;}\n .tab-content.active { display: block; }\n\n /* GRID PRINCIPAL DEL DASHBOARD */\n .dash-grid { display: grid; grid-template-columns: 1fr 350px; gap: 2rem; margin-bottom: 2.5rem; align-items: start;}\n \n /* PANELES DE INFORMACIÓN */\n .dash-panel { background: linear-gradient(145deg, rgba(20,20,25,0.8), rgba(10,10,15,0.9)); border: 1px solid var(--glass-border); border-radius: 24px; padding: 2rem; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 10px 30px rgba(0,0,0,0.5); backdrop-filter: blur(15px); display: flex; flex-direction: column; height: 100%; box-sizing: border-box;}\n .panel-title { color: white; font-size: 1.2rem; font-weight: 900; margin-top: 0; margin-bottom: 1.5rem; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 15px; display:flex; justify-content:space-between; align-items:center; text-transform:uppercase; letter-spacing:1px;}\n \n /* PRESENTACIÓN Y ENLACE AL PAPER */\n .presentation-text { color: #ccc; font-size: 1rem; line-height: 1.6; font-style: italic; border-left: 3px solid var(--accent-purple); padding-left: 15px; margin-bottom: 1.5rem; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;}\n .btn-open-paper { background: linear-gradient(135deg, rgba(224,64,251,0.1), rgba(0,176,255,0.1)); border: 1px solid var(--accent-purple); color: white; padding: 12px 20px; border-radius: 12px; font-weight: bold; cursor: pointer; transition: 0.3s; display:flex; justify-content:center; align-items:center; gap:10px; width: 100%; font-size: 0.95rem;}\n .btn-open-paper:hover { background: var(--accent-purple); box-shadow: 0 0 20px rgba(224,64,251,0.4); transform: translateY(-2px);}\n\n /* MINI MAP CONTAINER DRY */\n .mini-map-container { width: 100%; height: 300px; position: relative; border-radius:16px; border:1px solid #333; overflow:hidden; margin-bottom: 2rem;}\n\n /* KPIS */\n .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.2rem; margin-bottom: 2.5rem; }\n .kpi-card { background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); padding: 1.5rem 1rem; border-radius: 16px; text-align: center; transition: 0.3s; }\n .kpi-card:hover { transform: translateY(-3px); background: rgba(255,255,255,0.02); }\n .kpi-val { font-size: 2.2rem; font-weight: 900; display: block; margin-bottom: 5px; font-family: var(--font-mono); line-height: 1;}\n .kpi-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: bold; }\n\n /* TELEMETRÍA A2A */\n .ai-arbitrage-panel { background: rgba(0,0,0,0.4); border: 1px dashed var(--accent-blue); padding: 1.5rem; border-radius: 16px; display: flex; flex-direction:column; gap: 15px;}\n .ai-stat-row { display: flex; align-items: center; justify-content: space-between;}\n .ai-stat-lbl { font-size: 0.7rem; color: #aaa; text-transform: uppercase; font-weight: bold;}\n .ai-stat-val { font-size: 1.4rem; font-weight: 900; font-family: var(--font-mono); color: white; }\n .ai-avatar-badge { width: 35px; height: 35px; border-radius: 50%; background: var(--accent-purple); display:flex; justify-content:center; align-items:center; font-size: 1.2rem; border: 2px solid #111; margin-left: -10px; box-shadow: 0 2px 5px rgba(0,0,0,0.5);}\n .ai-avatar-badge:first-child { margin-left: 0; }\n\n /* MODAL IA */\n .modal-ia { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.9); z-index: 4000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px);}\n .modal-ia-content { background: var(--bg-dark); width: 90%; max-width: 800px; max-height: 85vh; border-radius: 20px; border: 1px solid var(--glass-border); display: flex; flex-direction: column; overflow:hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.8); border-top: 4px solid var(--accent-purple);}\n \n @media (max-width: 1024px) {\n .dash-grid { grid-template-columns: 1fr; }\n }\n @media (max-width: 768px) {\n .workspace-dash { padding: 90px 1rem 120px 1rem; }\n .dash-panel { padding: 1.5rem; }\n }\n </style>\n\n <div class=\"app-layout\">\n ${Sidebar.getHtml('/dashboard')}\n\n <main class=\"workspace-dash\">\n ${PageHeader.getHtml(headerConfig)}\n\n <div id=\"tab-overview\" class=\"tab-content ${this.currentTab === 'overview' ? 'active' : ''}\">\n \n <div class=\"dash-grid\">\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-purple); margin-bottom: 2rem;\">\n <div class=\"panel-title\"><span>📖 Propósito Fundacional</span> <div style=\"display:flex; gap:5px;\">${tagsHtml}</div></div>\n <div class=\"presentation-text\" id=\"pitchText\">${pitchText}</div>\n <button class=\"btn-open-paper\" id=\"btnOpenPaper\">📝 Desarrollar en Omni-Paper (Usenet)</button>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-blue); padding:0;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">🕸️ Topología del Ecosistema</div>\n <div class=\"mini-map-container\" id=\"dashMapContainer\">\n <div class=\"map-canvas map-svg-layer\" id=\"dashMapCanvas\">\n <svg id=\"dashMapSvg\">\n <defs>\n <marker id=\"arrow-tangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#00e676\"/></marker>\n <marker id=\"arrow-intangible-vis\" markerWidth=\"12\" markerHeight=\"8\" refX=\"10\" refY=\"4\" orient=\"auto\"><polygon points=\"0 0, 12 4, 0 8\" fill=\"#e040fb\"/></marker>\n </defs>\n <g id=\"dashMapPaths\"></g>\n </svg>\n </div>\n </div>\n <div style=\"padding: 0 2rem 2rem 2rem;\">\n <a href=\"/v8/map\" data-link style=\"color:var(--accent-blue); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Modificar Arquitectura &rarr;</a>\n </div>\n </div>\n </div>\n\n <div>\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); margin-bottom: 2rem;\">\n <div class=\"panel-title\">🤖 Enjambre IA Activo</div>\n <div style=\"display:flex; margin-bottom:1rem;\">\n ${aiAvatarsHtml}\n </div>\n \n <div class=\"ai-arbitrage-panel\">\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Tokens Consumidos</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-blue);\">${(totalTokens / 1000).toFixed(1)}k</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\">Gasto API (USD)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-red);\">$${realApiCost.toFixed(4)}</span>\n </div>\n <div class=\"ai-stat-row\" style=\"border-top: 1px dashed #444; padding-top: 15px; margin-top: 5px;\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-green);\">Valor (Slices) IA</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-green);\">€${aiGrossValue.toLocaleString()}</span>\n </div>\n <div class=\"ai-stat-row\">\n <span class=\"ai-stat-lbl\" style=\"color:var(--accent-purple);\">Ratio (REC)</span>\n <span class=\"ai-stat-val\" style=\"color:var(--accent-purple);\">${recRatio > 0 ? recRatio.toFixed(0) + 'x' : '0'}</span>\n </div>\n </div>\n </div>\n\n <section class=\"kpi-grid\">\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-green);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-green);\">${Math.round(totalSlices).toLocaleString()}</span>\n <span class=\"kpi-lbl\">Slices Minados</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid var(--accent-blue);\">\n <span class=\"kpi-val\" style=\"color: var(--accent-blue);\">${totalHours.toFixed(1)}h</span>\n <span class=\"kpi-lbl\">Trabajo Auditado</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">\n <span class=\"kpi-val\" style=\"color: ${resilience > 50 ? 'var(--accent-purple)' : 'var(--accent-red)'};\">${resilience}%</span>\n <span class=\"kpi-lbl\">Salud Estructural</span>\n </div>\n <div class=\"kpi-card\" style=\"border-bottom: 3px solid #888;\">\n <span class=\"kpi-val\" style=\"color: white;\">${project.usuarios ? project.usuarios.length : 1}</span>\n <span class=\"kpi-lbl\">Nodos en Colla</span>\n </div>\n </section>\n </div>\n </div>\n\n <div class=\"dash-panel\" style=\"border-top: 4px solid var(--accent-green); padding:0; overflow:hidden;\">\n <div class=\"panel-title\" style=\"padding: 2rem 2rem 0 2rem; border:none; margin-bottom:1rem;\">⚖️ Distribución de Equidad</div>\n <div id=\"dashLedgerContainer\" style=\"padding: 0 2rem 2rem 2rem;\"></div>\n <div style=\"background: rgba(0,230,118,0.1); padding: 15px 2rem; text-align:right;\">\n <a href=\"/v8/ledger\" data-link style=\"color:var(--accent-green); font-size:0.9rem; text-decoration:none; font-weight:bold;\">Ir a la Notaría (Ledger Completo) &rarr;</a>\n </div>\n </div>\n\n </div>\n\n <div id=\"tab-market\" class=\"tab-content ${this.currentTab === 'market' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem;\">\n <h2 style=\"color: white; margin-top: 0; font-size: 1.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; font-weight: 900; text-transform: uppercase;\">🎯 Mercado Interno (Vacantes)</h2>\n <p style=\"color:#888; font-size:0.95rem; margin-bottom:2rem; line-height:1.5;\">Roles vitales diseñados en la arquitectura que aún no tienen un talento humano o IA asignado. El <b>Dharma Coach</b> usa esto para reclutar talento.</p>\n <div>${vacantesHtml}</div>\n </div>\n </div>\n\n <div id=\"tab-settings\" class=\"tab-content ${this.currentTab === 'settings' ? 'active' : ''}\">\n <div class=\"glass-panel\" style=\"padding:3rem; text-align:center;\">\n <h2 style=\"color: var(--text-muted); margin-top:0;\">Módulo de Configuración de Red</h2>\n <p style=\"color:#888;\">Las opciones de gobernanza, APIs multimodales y Padrón de IAs se gestionan de forma segura y Zero-Trust desde la Consola Global.</p>\n <a href=\"/v8/settings\" data-link class=\"btn-primary\" style=\"display:inline-block; margin-top:2rem; text-decoration:none;\">Ir al Panteón Global</a>\n </div>\n </div>\n\n </main>\n </div>\n\n <div id=\"aiModal\" class=\"modal-ia\">\n <div class=\"modal-ia-content\">\n <div style=\"padding:20px 30px; border-bottom:1px solid rgba(255,255,255,0.05); display:flex; justify-content:space-between; align-items:center;\">\n <h2 id=\"aiModalTitle\" style=\"margin:0; font-size:1.2rem; color:var(--accent-purple); font-weight:900; text-transform:uppercase; letter-spacing:1px;\">Procesando...</h2>\n <button id=\"aiModalClose\" style=\"background:transparent; border:none; color:#aaa; cursor:pointer; font-size:1.5rem; transition:0.2s;\">✖</button>\n </div>\n <div style=\"padding: 30px; overflow-y: auto; color: #ccc; font-size: 1rem; line-height: 1.7; white-space: pre-wrap;\" id=\"aiModalBody\"></div>\n <div style=\"padding:20px 30px; border-top:1px solid rgba(255,255,255,0.05); display:flex; justify-content:flex-end; background:rgba(0,0,0,0.5);\">\n <button class=\"btn-primary\" id=\"btnDownloadPDF\" style=\"display:none; background:var(--accent-purple); color:white; border:none; padding:10px 20px; border-radius:12px; font-weight:bold; cursor:pointer;\">⬇️ DESCARGAR INFORME (.TXT)</button>\n </div>\n </div>\n </div>\n `;\n }\n\n getIcon(l) { return { '@anxaneta': '👑', '@aixecador': '🧭', '@dosos': '👁️', '@baixos': '⚙️', '@pinya': '🤝' }[l] || '💠'; }\n\n executeViewScript() {\n if (!this.activeProjectId) return;\n \n Sidebar.initListeners();\n PageHeader.execute();\n \n const state = store.getState();\n const project = state.projects.find(p => p.id === this.activeProjectId);\n if(!project) return;\n\n window.addEventListener('ph-tab-changed', (e) => {\n this.currentTab = e.detail.tabId;\n document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n const target = document.getElementById(`tab-${this.currentTab}`);\n if(target) target.classList.add('active');\n });\n\n // 🔥 MAGIA DRY 1: RENDERIZAR MAPA EN EL DASHBOARD\n const dashMapCanvas = document.getElementById('dashMapCanvas');\n const dashMapPaths = document.getElementById('dashMapPaths');\n if (dashMapCanvas && dashMapPaths && project.roles) {\n const mr = new MapRenderer(dashMapCanvas, dashMapPaths, { \n isMacro: true, \n isHeatmap: false,\n markerSuffix: 'vis',\n trimSize: 20\n });\n mr.setData(project.roles, project.vna_flows || []);\n }\n\n // 🔥 MAGIA DRY 2: RENDERIZAR LEDGER (SOLO PIE Y CAP TABLE)\n const dashLedgerContainer = document.getElementById('dashLedgerContainer');\n if (dashLedgerContainer) {\n const lr = new LedgerRenderer(dashLedgerContainer, {\n projectId: project.id,\n showHistory: false // Ocultamos la tabla de Blockchain para no saturar el Dashboard\n });\n lr.render();\n }\n\n // 🔥 CREACIÓN DINÁMICA DE WORK ORDER (IR AL PAPER)\n const btnOpenPaper = document.getElementById('btnOpenPaper');\n if (btnOpenPaper) {\n btnOpenPaper.addEventListener('click', async () => {\n // Creamos una Work Order \"Teórica\" en el vuelo (Una Tarea Huérfana de investigación)\n const newHash = 'wo_research_' + Math.random().toString(36).substr(2, 9);\n \n await store.dispatch({\n type: 'SPAWN_WORK_ORDER',\n payload: {\n projectId: this.activeProjectId,\n workOrder: {\n hash: newHash, \n flowId: null, // No pertenece a ningún SOP, es investigación libre\n comentario: `Investigación y Desarrollo del Manifiesto / Prompt del proyecto.`,\n status: 'pinged', // Directamente en curso\n realHours: 0,\n sprintId: project.activeSprintId,\n soc_checklist: [], \n assigneeId: state.session.activeUserId // Asignada al PO\n }\n }\n });\n\n // Redirigimos al Omni-Paper con el Hash cargado\n window.location.href = `/v8/paper?hash=${newHash}`;\n });\n }\n\n document.querySelectorAll('.btn-invite').forEach(btn => {\n btn.addEventListener('click', (e) => {\n const roleName = e.target.getAttribute('data-rolename');\n const email = prompt(`Invitar mercenario o Agente IA para el rol [${roleName}]. Introduce email o Alias (@id):`);\n if (email) alert(`Invitación enviada a ${email}. Pendiente de integración P2P.`);\n });\n });\n\n // Lógica Modal IA (Auditoría VNA & Equity)\n const modal = document.getElementById('aiModal');\n const modalBody = document.getElementById('aiModalBody');\n const modalTitle = document.getElementById('aiModalTitle');\n const btnDownload = document.getElementById('btnDownloadPDF');\n const modalCloseBtn = document.getElementById('aiModalClose');\n\n if (modalCloseBtn) modalCloseBtn.onclick = () => modal.style.display = 'none';\n\n const runAI = async (type) => {\n if (!modal) return;\n \n const provider = localStorage.getItem('tt_ai_provider') || 'deepseek';\n let apiKey = '';\n if (provider === 'deepseek') apiKey = localStorage.getItem('tt_key_deepseek');\n if (provider === 'openai') apiKey = localStorage.getItem('tt_key_openai');\n if (provider === 'gemini') apiKey = localStorage.getItem('tt_key_gemini');\n\n if (provider !== 'custom' && !apiKey) return alert(\"⚠️ Configura tu API Key en la Consola V14 antes de invocar al Orquestador.\");\n\n modal.style.display = 'flex';\n modalBody.innerHTML = `<div style=\"text-align:center; padding:4rem;\"><div style=\"font-size:4rem; animation: pulse 2s infinite;\">🧠</div><p style=\"color:var(--accent-purple); margin-top:1.5rem; font-family:var(--font-mono); font-weight:bold;\">Analizando el Ledger Fractal y la Topología...</p></div>`;\n btnDownload.style.display = 'none';\n modalTitle.innerText = type === 'audit' ? 'Auditoría VNA & Equity' : 'Pacto de Socios (Slicing Pie)';\n\n const harvest = store.calculateHarvest(project.id) || [];\n const totalSlices = harvest.reduce((sum, h) => sum + h.slices, 0);\n \n let capTableDetails = [\"El Ledger está vacío.\"];\n if (harvest.length > 0 && totalSlices > 0) {\n capTableDetails = harvest.map(h => {\n const u = store.getState().globalUsers?.find(gu => gu.id === (h.user || h.userId));\n const userName = u ? u.name : (h.user || h.userId || 'Desconocido');\n return `- Nodo: ${userName} | Equidad: ${((h.slices / totalSlices) * 100).toFixed(2)}%`;\n });\n }\n\n const dataPayload = {\n nombre_ecosistema: project.nombre,\n arquetipo_gobernanza: project.archetype,\n vision_fundacional: project.presentation || project.prompt || \"Sin definir\",\n roles: project.roles.map(r => r.name),\n cap_table_actual: capTableDetails\n };\n\n let systemPrompt = \"Eres un Master Architect de DAOs.\";\n if (type === 'audit') {\n systemPrompt += `\\nEvalúa si la topología y distribución de Slices refleja un ecosistema sano basado en la teoría Slicing Pie.`;\n } else {\n systemPrompt += `\\nRedacta un Pacto de Socios formal (Smart Contract / Legal Draft) basado en el Cap Table adjunto.`;\n }\n\n try {\n // LLamada Legacy API directa (Refactor inminente a Orchestrator en Sprints futuros)\n let text = \"\";\n if (provider === 'deepseek') {\n const res = await fetch(`https://api.deepseek.com/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"deepseek-chat\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'openai') {\n const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: \"gpt-4o-mini\", messages: [{ role: \"system\", content: systemPrompt }, { role: \"user\", content: JSON.stringify(dataPayload) }] }) });\n text = (await res.json()).choices[0].message.content;\n } else if (provider === 'gemini') {\n const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: `${systemPrompt}\\n\\nJSON: ${JSON.stringify(dataPayload)}` }] }] }) });\n text = (await res.json()).candidates[0].content.parts[0].text;\n }\n\n modalBody.innerHTML = `<div style=\"font-family:var(--font-main);\">${text.replace(/\\n/g, '<br>')}</div>`;\n btnDownload.style.display = 'block';\n btnDownload.onclick = () => {\n const a = document.createElement('a'); \n a.href = URL.createObjectURL(new Blob([text], {type:\"text/plain\"})); \n a.download = `${type}_${project.nombre.replace(/ /g,'_')}.txt`; \n a.click();\n };\n } catch (e) { \n modalBody.innerHTML = `<div style=\"text-align:center; padding:2rem;\"><div style=\"font-size:3rem; margin-bottom:1rem;\">⚠️</div><h3 style=\"color:var(--accent-red);\">Fallo Neural</h3><p style=\"color:#888;\">${e.message}</p></div>`; \n }\n };\n\n window.addEventListener('ph-magic-action', (e) => {\n if (e.detail.actionId === 'audit') runAI('audit');\n if (e.detail.actionId === 'legal') runAI('legal');\n });\n }\n}\n"}}}}