1 line
182 KiB
JSON
1 line
182 KiB
JSON
{"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 →</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) →</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 →</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) →</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"}}}} |