trackops 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -6
- package/lib/control.js +1 -0
- package/lib/init.js +12 -10
- package/lib/opera.js +3 -3
- package/lib/server.js +141 -0
- package/lib/skills.js +1 -1
- package/package.json +16 -3
- package/templates/etapa/agent.md +2 -2
- package/templates/etapa/references/etapa-cycle.md +1 -1
- package/templates/opera/agent.md +1 -1
- package/templates/skills/project-starter-skill/SKILL.md +5 -3
- package/ui/css/base.css +266 -0
- package/ui/css/charts.css +327 -0
- package/ui/css/components.css +570 -0
- package/ui/css/panels.css +953 -0
- package/ui/css/tokens.css +227 -0
- package/ui/favicon.svg +5 -0
- package/ui/index.html +91 -351
- package/ui/js/api.js +203 -0
- package/ui/js/app.js +199 -0
- package/ui/js/console-logger.js +172 -0
- package/ui/js/icons.js +104 -0
- package/ui/js/onboarding.js +437 -0
- package/ui/js/router.js +125 -0
- package/ui/js/state.js +129 -0
- package/ui/js/theme.js +100 -0
- package/ui/js/time-tracker.js +248 -0
- package/ui/js/utils.js +172 -0
- package/ui/js/views/board.js +254 -0
- package/ui/js/views/execution.js +256 -0
- package/ui/js/views/flash.js +47 -0
- package/ui/js/views/insights.js +339 -0
- package/ui/js/views/overview.js +364 -0
- package/ui/js/views/settings.js +243 -0
- package/ui/js/views/sidebar.js +132 -0
- package/ui/js/views/skills.js +162 -0
- package/ui/js/views/tasks.js +405 -0
- package/ui/js/views/topbar.js +183 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sidebar.js — Sidebar de navegación lateral
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { icon } from '../icons.js';
|
|
6
|
+
import * as state from '../state.js';
|
|
7
|
+
import * as consoleLogger from '../console-logger.js';
|
|
8
|
+
import * as onboarding from '../onboarding.js';
|
|
9
|
+
|
|
10
|
+
const NAV_ITEMS = [
|
|
11
|
+
{ id: 'overview', label: 'Resumen', icon: 'dashboard', section: 'menu' },
|
|
12
|
+
{ id: 'tasks', label: 'Tareas', icon: 'tasks', section: 'menu', badge: true },
|
|
13
|
+
{ id: 'board', label: 'Tablero', icon: 'board', section: 'menu' },
|
|
14
|
+
{ id: 'execution', label: 'Ejecución', icon: 'execution', section: 'menu' },
|
|
15
|
+
{ id: 'skills', label: 'Habilidades', icon: 'zap', section: 'menu' },
|
|
16
|
+
{ id: 'insights', label: 'Analíticas', icon: 'insights', section: 'menu' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const GENERAL_ITEMS = [
|
|
20
|
+
{ id: 'settings', label: 'Configuración', icon: 'settings' },
|
|
21
|
+
{ id: 'help', label: 'Ayuda & Tour', icon: 'help', action: 'tour' },
|
|
22
|
+
{ id: 'console', label: 'Registros', icon: 'console', action: 'console', badge: 'error' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Renderiza el sidebar completo */
|
|
26
|
+
export function render() {
|
|
27
|
+
const el = document.getElementById('sidebar');
|
|
28
|
+
if (!el) return;
|
|
29
|
+
|
|
30
|
+
el.innerHTML = `
|
|
31
|
+
<nav class="sidebar" aria-label="Navegación principal">
|
|
32
|
+
<!-- Logo -->
|
|
33
|
+
<a href="#" class="sidebar-logo" aria-label="TrackOps — Ir al inicio">
|
|
34
|
+
<div class="sidebar-logo-icon" aria-hidden="true">
|
|
35
|
+
${icon('infinity', 20)}
|
|
36
|
+
</div>
|
|
37
|
+
<span class="sidebar-logo-name">TrackOps</span>
|
|
38
|
+
</a>
|
|
39
|
+
|
|
40
|
+
<!-- MENU -->
|
|
41
|
+
<div class="sidebar-section">
|
|
42
|
+
<p class="sidebar-section-label">Menú</p>
|
|
43
|
+
<ul class="sidebar-nav" role="list">
|
|
44
|
+
${NAV_ITEMS.map(item => _renderNavItem(item)).join('')}
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- GENERAL -->
|
|
49
|
+
<div class="sidebar-footer">
|
|
50
|
+
<p class="sidebar-section-label" style="padding-bottom:var(--space-2)">General</p>
|
|
51
|
+
${GENERAL_ITEMS.map(item => _renderGeneralItem(item)).join('')}
|
|
52
|
+
</div>
|
|
53
|
+
</nav>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
_bindEvents(el);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _renderNavItem(item) {
|
|
60
|
+
const activeView = state.get('activeView');
|
|
61
|
+
const isActive = activeView === item.id;
|
|
62
|
+
const pendingCount = _getPendingCount(item);
|
|
63
|
+
|
|
64
|
+
return `
|
|
65
|
+
<li role="listitem">
|
|
66
|
+
<button
|
|
67
|
+
class="nav-item ${isActive ? 'is-active' : ''}"
|
|
68
|
+
data-view="${item.id}"
|
|
69
|
+
type="button"
|
|
70
|
+
aria-current="${isActive ? 'page' : 'false'}"
|
|
71
|
+
aria-label="${item.label}${pendingCount ? `, ${pendingCount} pendientes` : ''}"
|
|
72
|
+
>
|
|
73
|
+
<span class="nav-item-icon" aria-hidden="true">${icon(item.icon, 18)}</span>
|
|
74
|
+
<span class="nav-item-label">${item.label}</span>
|
|
75
|
+
${pendingCount ? `<span class="nav-item-badge" aria-label="${pendingCount} tareas">${pendingCount}</span>` : ''}
|
|
76
|
+
</button>
|
|
77
|
+
</li>
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _renderGeneralItem(item) {
|
|
82
|
+
return `
|
|
83
|
+
<button
|
|
84
|
+
class="nav-item"
|
|
85
|
+
data-action="${item.action || ''}"
|
|
86
|
+
data-view="${item.action ? '' : item.id}"
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label="${item.label}"
|
|
89
|
+
${item.action === 'console' ? 'id="sidebar-console-btn"' : ''}
|
|
90
|
+
>
|
|
91
|
+
<span class="nav-item-icon" aria-hidden="true">${icon(item.icon, 18)}</span>
|
|
92
|
+
<span class="nav-item-label">${item.label}</span>
|
|
93
|
+
${item.badge === 'error' ? `<span class="nav-item-badge danger" id="sidebar-console-badge" aria-label="errores" style="display:none">0</span>` : ''}
|
|
94
|
+
</button>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _getPendingCount(item) {
|
|
99
|
+
if (!item.badge) return 0;
|
|
100
|
+
const payload = state.getPayload();
|
|
101
|
+
if (!payload) return 0;
|
|
102
|
+
if (item.id === 'tasks') return payload.derived?.totals?.pending || 0;
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _bindEvents(el) {
|
|
107
|
+
// Acciones especiales (no nav-view)
|
|
108
|
+
el.addEventListener('click', e => {
|
|
109
|
+
const btn = e.target.closest('[data-action]');
|
|
110
|
+
if (!btn) return;
|
|
111
|
+
const action = btn.dataset.action;
|
|
112
|
+
if (action === 'tour') {
|
|
113
|
+
onboarding.reset();
|
|
114
|
+
onboarding.show();
|
|
115
|
+
} else if (action === 'console') {
|
|
116
|
+
consoleLogger.toggle();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Refresca solo los badges sin re-renderizar todo */
|
|
122
|
+
export function updateBadges() {
|
|
123
|
+
const payload = state.getPayload();
|
|
124
|
+
if (!payload) return;
|
|
125
|
+
|
|
126
|
+
const pending = payload.derived?.totals?.pending || 0;
|
|
127
|
+
const tasksBtn = document.querySelector('[data-view="tasks"] .nav-item-badge');
|
|
128
|
+
if (tasksBtn) {
|
|
129
|
+
tasksBtn.textContent = pending;
|
|
130
|
+
tasksBtn.style.display = pending ? '' : 'none';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as api from '../api.js';
|
|
2
|
+
import * as flash from './flash.js';
|
|
3
|
+
|
|
4
|
+
export function init() {
|
|
5
|
+
// Sin estado local init por ahora
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function render() {
|
|
9
|
+
return `
|
|
10
|
+
<div class="view-enter">
|
|
11
|
+
<header class="section-header">
|
|
12
|
+
<div>
|
|
13
|
+
<h2 class="section-title">Centro de habilidades</h2>
|
|
14
|
+
<p class="section-desc">Gestiona las habilidades de tu copiloto e integra nuevos flujos de la comunidad.</p>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="actions">
|
|
17
|
+
<button class="btn btn-ghost btn-sm" id="btn-refresh-skills">
|
|
18
|
+
<svg viewBox="0 0 24 24" fill="none" class="icon" style="width:16px;height:16px"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
19
|
+
Refrescar
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<div class="grid-split" style="grid-template-columns: 1fr 1fr; gap: var(--space-6);">
|
|
25
|
+
|
|
26
|
+
<!-- Panel Izquierdo: Capacidades Locales -->
|
|
27
|
+
<div class="panel">
|
|
28
|
+
<div class="panel-header">
|
|
29
|
+
<h3 class="panel-title">Capacidades Instaladas</h3>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="skills-local-list" class="stack" style="gap: var(--space-4); max-height: calc(100vh - 200px); overflow-y: auto; padding: 2px;">
|
|
32
|
+
<div class="card"><div class="card-body"><p class="text-sm color-muted">Cargando...</p></div></div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Panel Derecho: Discover (skills.sh) -->
|
|
37
|
+
<div class="panel" style="border-color: var(--accent);">
|
|
38
|
+
<div class="panel-header" style="justify-content: space-between;">
|
|
39
|
+
<h3 class="panel-title" style="color: var(--accent);">✨ Descubrir</h3>
|
|
40
|
+
<span class="badge badge-success">Recomendado</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div id="skills-discover-list" class="stack" style="gap: var(--space-4); max-height: calc(100vh - 200px); overflow-y: auto; padding: 2px;">
|
|
43
|
+
<div class="card"><div class="card-body"><p class="text-sm color-muted">Conectando...</p></div></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function bindEvents() {
|
|
53
|
+
const btn = document.getElementById('btn-refresh-skills');
|
|
54
|
+
if (btn) {
|
|
55
|
+
btn.addEventListener('click', loadData);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadData() {
|
|
60
|
+
const localEl = document.getElementById('skills-local-list');
|
|
61
|
+
const discoverEl = document.getElementById('skills-discover-list');
|
|
62
|
+
|
|
63
|
+
if (!localEl || !discoverEl) return;
|
|
64
|
+
|
|
65
|
+
localEl.innerHTML = `<p class="text-sm text-muted">Cargando datos locales...</p>`;
|
|
66
|
+
discoverEl.innerHTML = `<p class="text-sm text-muted">Conectando a skills.sh...</p>`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const [localRes, discoverRes] = await Promise.all([
|
|
70
|
+
api.fetchSkillsLocal(),
|
|
71
|
+
api.fetchSkillsDiscover()
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// DOM Locales
|
|
75
|
+
if (localRes.ok && localRes.skills) {
|
|
76
|
+
if (localRes.skills.length === 0) {
|
|
77
|
+
localEl.innerHTML = `
|
|
78
|
+
<div class="empty-state">
|
|
79
|
+
<div class="empty-icon">🧠</div>
|
|
80
|
+
<p class="empty-title">Sin habilidades locales</p>
|
|
81
|
+
<p class="empty-desc" style="font-size:var(--text-xs); color:var(--text-muted)">Tu agente no tiene skills especializadas. Instala alguna desde el panel derecho.</p>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
} else {
|
|
85
|
+
localEl.innerHTML = localRes.skills.map(s => `
|
|
86
|
+
<div class="card">
|
|
87
|
+
<div class="card-body">
|
|
88
|
+
<div style="display: flex; gap: 12px; align-items: center;">
|
|
89
|
+
<div style="font-size: 24px;">⚙️</div>
|
|
90
|
+
<div>
|
|
91
|
+
<h4 style="font-size: var(--text-sm); font-weight: 600; margin-bottom: 4px;">${s.title}</h4>
|
|
92
|
+
<p style="font-size: var(--text-xs); color: var(--text-secondary);">${s.description || 'Sin descripción'}</p>
|
|
93
|
+
<code style="font-size: 10px; margin-top: 8px; display: inline-block; background: var(--surface-3); padding: 2px 4px; border-radius: 4px;">${s.path}</code>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`).join('');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// DOM Discover
|
|
103
|
+
if (discoverRes.ok && discoverRes.catalog) {
|
|
104
|
+
// Filter out already installed
|
|
105
|
+
const installedIds = (localRes.skills || []).map(s => s.id);
|
|
106
|
+
const available = discoverRes.catalog.filter(c => !installedIds.includes(c.id));
|
|
107
|
+
|
|
108
|
+
if (available.length === 0) {
|
|
109
|
+
discoverEl.innerHTML = `
|
|
110
|
+
<div class="empty-state">
|
|
111
|
+
<div class="empty-icon">✨</div>
|
|
112
|
+
<p class="empty-title" style="margin-bottom:var(--space-2)">Todo instalado</p>
|
|
113
|
+
<p class="empty-desc" style="font-size:var(--text-xs); color:var(--text-muted)">Ya tienes instaladas todas las habilidades recomendadas.</p>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
} else {
|
|
117
|
+
discoverEl.innerHTML = available.map(s => `
|
|
118
|
+
<div class="card" style="border: 1px dashed var(--border-accent);">
|
|
119
|
+
<div class="card-body">
|
|
120
|
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;">
|
|
121
|
+
<div style="flex:1;">
|
|
122
|
+
<h4 style="font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">${s.title}</h4>
|
|
123
|
+
<p style="font-size: var(--text-xs); color: var(--text-secondary);">${s.description}</p>
|
|
124
|
+
</div>
|
|
125
|
+
<button class="btn btn-primary btn-sm btn-install-skill" data-id="${s.id}" style="padding: 4px 12px; font-size: 11px;">Instalar</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
`).join('');
|
|
130
|
+
|
|
131
|
+
// Bind events
|
|
132
|
+
discoverEl.querySelectorAll('.btn-install-skill').forEach(btn => {
|
|
133
|
+
btn.addEventListener('click', async (e) => {
|
|
134
|
+
const id = e.target.dataset.id;
|
|
135
|
+
const originalText = e.target.textContent;
|
|
136
|
+
e.target.textContent = 'Instalando...';
|
|
137
|
+
e.target.disabled = true;
|
|
138
|
+
|
|
139
|
+
const res = await api.installSkill(id).catch(err => {
|
|
140
|
+
flash.show('error', err.message);
|
|
141
|
+
return null;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (res && res.ok) {
|
|
145
|
+
flash.show('success', `Skill ${id} instalada correctamente en /.agents/skills`);
|
|
146
|
+
loadData(); // recargar
|
|
147
|
+
} else {
|
|
148
|
+
e.target.textContent = originalText;
|
|
149
|
+
e.target.disabled = false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(err);
|
|
158
|
+
flash.show('error', 'Error conectando con la API de Skills.');
|
|
159
|
+
localEl.innerHTML = `<p class="color-danger text-sm">Error cargando.</p>`;
|
|
160
|
+
discoverEl.innerHTML = `<p class="color-danger text-sm">Error conectando.</p>`;
|
|
161
|
+
}
|
|
162
|
+
}
|