trackops 2.0.3 → 2.0.4

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.
Files changed (45) hide show
  1. package/README.md +238 -0
  2. package/lib/init.js +2 -2
  3. package/lib/locale.js +41 -17
  4. package/lib/opera-bootstrap.js +68 -7
  5. package/lib/opera.js +10 -2
  6. package/lib/registry.js +18 -0
  7. package/lib/server.js +312 -207
  8. package/locales/en.json +4 -0
  9. package/locales/es.json +4 -0
  10. package/package.json +1 -1
  11. package/skills/trackops/locales/en/references/activation.md +15 -0
  12. package/skills/trackops/locales/en/references/troubleshooting.md +12 -0
  13. package/skills/trackops/references/activation.md +15 -0
  14. package/skills/trackops/references/troubleshooting.md +12 -0
  15. package/skills/trackops/skill.json +2 -2
  16. package/ui/css/base.css +19 -1
  17. package/ui/css/charts.css +106 -8
  18. package/ui/css/components.css +554 -17
  19. package/ui/css/onboarding.css +133 -0
  20. package/ui/css/panels.css +345 -406
  21. package/ui/css/terminal.css +125 -0
  22. package/ui/css/timeline.css +58 -0
  23. package/ui/css/tokens.css +170 -113
  24. package/ui/index.html +3 -0
  25. package/ui/js/api.js +49 -13
  26. package/ui/js/app.js +28 -32
  27. package/ui/js/charts.js +526 -0
  28. package/ui/js/filters.js +247 -0
  29. package/ui/js/icons.js +82 -57
  30. package/ui/js/keyboard.js +229 -0
  31. package/ui/js/onboarding.js +33 -42
  32. package/ui/js/router.js +20 -3
  33. package/ui/js/views/board.js +84 -114
  34. package/ui/js/views/dashboard.js +870 -0
  35. package/ui/js/views/projects.js +745 -0
  36. package/ui/js/views/scrum.js +476 -0
  37. package/ui/js/views/settings.js +197 -247
  38. package/ui/js/views/sidebar.js +37 -31
  39. package/ui/js/views/tasks.js +218 -101
  40. package/ui/js/views/timeline.js +265 -0
  41. package/ui/js/views/topbar.js +94 -107
  42. package/ui/app.js +0 -950
  43. package/ui/js/views/insights.js +0 -340
  44. package/ui/js/views/overview.js +0 -369
  45. package/ui/styles.css +0 -688
@@ -1,340 +0,0 @@
1
- /**
2
- * insights.js — Vista de Analytics: progreso, actividad, hallazgos, KPI de salud
3
- */
4
-
5
- import { icon } from '../icons.js';
6
- import * as state from '../state.js';
7
- import * as timeTracker from '../time-tracker.js';
8
- import { esc, formatDate, formatDurationShort, extractHistory } from '../utils.js';
9
- import { t } from '../i18n.js';
10
-
11
- export async function render() {
12
- const payload = state.getPayload();
13
- if (!payload) return `<div class="empty-state" style="margin:3rem">${t('ui.insights.noData', {}, 'No project data.')}</div>`;
14
-
15
- const { derived, control, runtime } = payload;
16
- const statusLabels = state.getStatusLabels();
17
- const history = extractHistory(control.tasks).slice(0, 20);
18
-
19
- return `
20
- <div class="view-enter">
21
- <div class="section-header">
22
- <div class="section-header-left">
23
- <p class="eyebrow">${t('ui.insights.eyebrow', {}, 'Insights')}</p>
24
- <h2>${t('ui.insights.title', {}, 'Project insights')}</h2>
25
- </div>
26
- </div>
27
-
28
- <!-- Fila 1: Health Grid + Distribution -->
29
- <div class="grid-2" style="margin-bottom:var(--space-5)">
30
-
31
- <!-- KPI de salud -->
32
- <div class="chart-card stagger-1">
33
- <div class="section-header" style="margin-bottom:var(--space-3)">
34
- <p class="chart-title">${t('ui.insights.health', {}, 'Operational health')}</p>
35
- </div>
36
- ${_renderHealthGrid(derived, runtime, payload.docsDirty)}
37
- </div>
38
-
39
- <!-- Distribución por estado -->
40
- <div class="chart-card stagger-2">
41
- <p class="chart-title" style="margin-bottom:var(--space-4)">${t('ui.insights.distribution', {}, 'Distribution by status')}</p>
42
- ${_renderDistribution(derived.totals, statusLabels)}
43
- </div>
44
-
45
- </div>
46
-
47
- <!-- Fila 2: Time entries + Phase progress -->
48
- <div class="grid-2" style="margin-bottom:var(--space-5)">
49
-
50
- <!-- Time tracking summary -->
51
- <div class="chart-card stagger-3">
52
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4)">
53
- <p class="chart-title">${t('ui.insights.time', {}, 'Time tracking')}</p>
54
- <span class="badge badge-accent">${icon('clock', 12)} ${t('ui.insights.today', {}, 'Today')}</span>
55
- </div>
56
- ${_renderTimeTracking()}
57
- </div>
58
-
59
- <!-- Phase progress detallado -->
60
- <div class="chart-card stagger-4">
61
- <p class="chart-title" style="margin-bottom:var(--space-4)">${t('ui.insights.phaseProgress', {}, 'Progress by phase')}</p>
62
- ${_renderPhaseProgress(derived.phaseStats)}
63
- </div>
64
-
65
- </div>
66
-
67
- <!-- Fila 3: Activity + Findings -->
68
- <div class="grid-2">
69
-
70
- <!-- Activity timeline -->
71
- <div class="chart-card stagger-1">
72
- <p class="chart-title" style="margin-bottom:var(--space-4)">${t('ui.insights.recentActivity', {}, 'Recent activity')}</p>
73
- ${_renderActivityTimeline(history, statusLabels)}
74
- </div>
75
-
76
- <!-- Hallazgos + Decisiones -->
77
- <div style="display:flex;flex-direction:column;gap:var(--space-4)">
78
-
79
- <!-- Findings -->
80
- <div class="chart-card stagger-2">
81
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4)">
82
- <p class="chart-title">${t('ui.insights.findings', {}, 'Findings')}</p>
83
- <span class="badge badge-${derived.openFindings?.length ? 'warning' : 'success'}">
84
- ${t('ui.insights.openCount', { count: (derived.openFindings || []).length }, `${(derived.openFindings || []).length} open`)}
85
- </span>
86
- </div>
87
- ${_renderFindings(derived.openFindings || [], derived.resolvedFindings || [])}
88
- </div>
89
-
90
- <!-- Decisiones pendientes -->
91
- ${(control.decisionsPending || []).length > 0 ? `
92
- <div class="chart-card stagger-3">
93
- <p class="chart-title" style="margin-bottom:var(--space-4)">${t('ui.insights.pendingDecisions', {}, 'Pending decisions')}</p>
94
- ${_renderDecisions(control.decisionsPending)}
95
- </div>
96
- ` : ''}
97
-
98
- </div>
99
- </div>
100
- </div>
101
- `;
102
- }
103
-
104
- // ─────────────────────────────── HEALTH ─────────────────────────────────────
105
-
106
- function _renderHealthGrid(derived, runtime, docsDirty) {
107
- const totals = derived.totals;
108
- const completionRate = totals.all ? Math.round((totals.completed / totals.all) * 100) : 0;
109
- const blockerRate = totals.all ? Math.round((totals.blocked / totals.all) * 100) : 0;
110
- const openFindings = (derived.openFindings || []).length;
111
-
112
- const items = [
113
- { label: 'Tasa completada', value: `${completionRate}%`, cls: completionRate >= 75 ? 'good' : completionRate >= 40 ? '' : 'bad' },
114
- { label: 'Presión de bloqueos',value: `${blockerRate}%`, cls: blockerRate === 0 ? 'good' : blockerRate > 20 ? 'bad' : 'warn' },
115
- { label: 'En Progreso', value: String(totals.inProgress),cls: totals.inProgress > 0 ? 'good' : '' },
116
- { label: 'En Revisión', value: String(totals.inReview), cls: '' },
117
- { label: 'Hallazgos abiertos', value: String(openFindings), cls: openFindings === 0 ? 'good' : 'warn' },
118
- { label: 'Desfase documental', value: (docsDirty || []).length ? `${(docsDirty).length} archivos` : 'OK', cls: docsDirty?.length ? 'warn' : 'good' },
119
- { label: 'Por delante remoto', value: String(runtime?.ahead || 0), cls: runtime?.ahead > 0 ? 'warn' : 'good' },
120
- { label: 'Por detrás remoto', value: String(runtime?.behind || 0), cls: runtime?.behind > 0 ? 'bad' : 'good' },
121
- ];
122
-
123
- return `
124
- <div class="health-grid">
125
- ${items.map(({ label, value, cls }) => `
126
- <div class="health-card">
127
- <p class="health-card-label">${esc(label)}</p>
128
- <p class="health-card-value ${cls}" title="${esc(value)}">${esc(value)}</p>
129
- </div>
130
- `).join('')}
131
- </div>
132
- `;
133
- }
134
-
135
- // ─────────────────────────────── DISTRIBUCIÓN ───────────────────────────────
136
-
137
- function _renderDistribution(totals, statusLabels) {
138
- const all = totals.all || 1;
139
- const rows = [
140
- { id: 'completed', label: statusLabels.completed || 'Completado', value: totals.completed, cls: 'fill-success' },
141
- { id: 'in_progress',label: statusLabels.in_progress|| 'En progreso', value: totals.inProgress, cls: '' },
142
- { id: 'in_review', label: statusLabels.in_review || 'En revisión', value: totals.inReview, cls: '' },
143
- { id: 'pending', label: statusLabels.pending || 'Pendiente', value: totals.pending, cls: 'fill-warning' },
144
- { id: 'blocked', label: statusLabels.blocked || 'Bloqueado', value: totals.blocked, cls: 'fill-danger' },
145
- { id: 'cancelled', label: statusLabels.cancelled || 'Cancelado', value: totals.cancelled, cls: '' },
146
- ].filter(r => r.value > 0);
147
-
148
- return `
149
- <div class="bar-chart">
150
- ${rows.map(r => {
151
- const pct = Math.max(2, Math.round((r.value / all) * 100));
152
- return `
153
- <div class="bar-row" role="group" aria-label="${r.label}: ${r.value} (${pct}%)">
154
- <span class="bar-label truncate">${esc(r.label)}</span>
155
- <div class="bar-track" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100">
156
- <div class="bar-fill ${r.cls}" style="width:${pct}%"></div>
157
- </div>
158
- <span class="bar-value">${r.value}</span>
159
- </div>
160
- `;
161
- }).join('')}
162
- </div>
163
- `;
164
- }
165
-
166
- // ─────────────────────────────── TIME TRACKING ──────────────────────────────
167
-
168
- function _renderTimeTracking() {
169
- const entries = state.get('timeEntries');
170
-
171
- if (!entries.length) {
172
- return `
173
- <div class="empty-state" style="padding:var(--space-6)">
174
- ${icon('clock', 24)}
175
- <p>No hay registros de tiempo aún.</p>
176
- <p class="text-muted" style="font-size:var(--text-xs)">Usa el seguimiento de tiempo en Resumen para registrar tiempo por tarea.</p>
177
- </div>
178
- `;
179
- }
180
-
181
- // Agrupar por tarea
182
- const byTask = new Map();
183
- for (const e of entries) {
184
- if (!byTask.has(e.taskId)) byTask.set(e.taskId, { taskId: e.taskId, taskTitle: e.taskTitle, total: 0, count: 0 });
185
- const rec = byTask.get(e.taskId);
186
- rec.total += e.durationMs || 0;
187
- rec.count++;
188
- }
189
-
190
- const maxMs = Math.max(...[...byTask.values()].map(r => r.total), 1);
191
-
192
- return `
193
- <div class="bar-chart">
194
- ${[...byTask.values()].slice(0, 10).map(r => {
195
- const pct = Math.max(4, Math.round((r.total / maxMs) * 100));
196
- return `
197
- <div class="bar-row">
198
- <span class="bar-label truncate" title="${esc(r.taskTitle)}">${esc(r.taskTitle)}</span>
199
- <div class="bar-track">
200
- <div class="bar-fill fill-info" style="width:${pct}%"></div>
201
- </div>
202
- <span class="bar-value">${formatDurationShort(r.total)}</span>
203
- </div>
204
- `;
205
- }).join('')}
206
- </div>
207
- <p class="text-muted" style="font-size:var(--text-xs);margin-top:var(--space-3)">
208
- ${entries.length} registros en total
209
- </p>
210
- `;
211
- }
212
-
213
- // ─────────────────────────────── PHASE PROGRESS ─────────────────────────────
214
-
215
- function _renderPhaseProgress(phaseStats) {
216
- if (!phaseStats?.length) {
217
- return '<p class="text-muted" style="font-size:var(--text-sm)">Sin fases configuradas.</p>';
218
- }
219
- return `
220
- <div class="phase-chart">
221
- ${phaseStats.map(p => {
222
- const pct = p.total ? Math.round((p.completed / p.total) * 100) : 0;
223
- const cls = pct === 100 ? 'done' : pct > 0 ? 'active' : 'partial';
224
- return `
225
- <div class="phase-row">
226
- <div class="phase-row-header">
227
- <div>
228
- <span class="phase-name">${esc(p.id)} · ${esc(p.label)}</span>
229
- </div>
230
- <span class="phase-progress">${p.completed}/${p.total} (${pct}%)</span>
231
- </div>
232
- <div class="phase-track" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100">
233
- <div class="phase-fill ${cls}" style="width:${pct}%"></div>
234
- </div>
235
- <div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
236
- ${p.remaining === 0
237
- ? `<span class="badge badge-success">Completada</span>`
238
- : `<span class="badge badge-muted">${p.remaining} pendientes</span>`}
239
- </div>
240
- </div>
241
- `;
242
- }).join('')}
243
- </div>
244
- `;
245
- }
246
-
247
- // ─────────────────────────────── ACTIVITY ───────────────────────────────────
248
-
249
- function _renderActivityTimeline(history, statusLabels) {
250
- if (!history.length) {
251
- return '<div class="empty-state" style="padding:var(--space-5)">Sin actividad reciente.</div>';
252
- }
253
-
254
- const actionIcon = {
255
- create: 'plus',
256
- start: 'play',
257
- review: 'alertCircle',
258
- complete: 'checkCircle',
259
- block: 'shield',
260
- pending: 'clock',
261
- cancel: 'x',
262
- note: 'fileText',
263
- edit: 'edit',
264
- };
265
-
266
- const actionColor = {
267
- create: 'var(--accent)',
268
- start: 'var(--info)',
269
- review: 'var(--warning)',
270
- complete: 'var(--success)',
271
- block: 'var(--danger)',
272
- cancel: 'var(--text-muted)',
273
- note: 'var(--accent)',
274
- edit: 'var(--text-secondary)',
275
- pending: 'var(--warning)',
276
- };
277
-
278
- return `
279
- <div class="stack" style="max-height:420px;overflow-y:auto">
280
- ${history.map(h => {
281
- const ic = actionIcon[h.action] || 'info';
282
- const col = actionColor[h.action] || 'var(--accent)';
283
- return `
284
- <div class="activity-item">
285
- <div class="activity-icon" style="color:${col}">
286
- ${icon(ic, 16)}
287
- </div>
288
- <div class="activity-content">
289
- <p class="activity-action">${esc(h.action)}${h.note ? ` — ${esc(h.note)}` : ''}</p>
290
- <p class="activity-task">${esc(h.taskTitle)} (${esc(h.taskId)})</p>
291
- <p class="activity-time">${formatDate(h.at)}</p>
292
- </div>
293
- </div>
294
- `;
295
- }).join('')}
296
- </div>
297
- `;
298
- }
299
-
300
- // ─────────────────────────────── FINDINGS ───────────────────────────────────
301
-
302
- function _renderFindings(open, resolved) {
303
- if (!open.length && !resolved.length) {
304
- return `<div class="empty-state" style="padding:var(--space-4)">${icon('checkCircle', 20)} Sin hallazgos.</div>`;
305
- }
306
-
307
- return `
308
- <div class="stack stack-sm">
309
- ${open.map(f => `
310
- <div class="finding-item severity-${(f.severity || 'medium').toLowerCase()}">
311
- <div style="display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);margin-bottom:var(--space-1)">
312
- <p style="font-size:var(--text-sm);font-weight:700">${esc(f.title)}</p>
313
- <span class="badge badge-warning">${esc(f.severity?.toUpperCase() || 'MEDIUM')}</span>
314
- </div>
315
- <p style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(f.detail || '')}</p>
316
- ${f.impact ? `<p style="font-size:var(--text-xs);color:var(--text-muted);margin-top:var(--space-1)">Impacto: ${esc(f.impact)}</p>` : ''}
317
- </div>
318
- `).join('')}
319
- ${resolved.length > 0 ? `<p class="label-sm" style="margin-top:var(--space-3)">${resolved.length} resueltos</p>` : ''}
320
- </div>
321
- `;
322
- }
323
-
324
- // ─────────────────────────────── DECISIONES ─────────────────────────────────
325
-
326
- function _renderDecisions(decisions) {
327
- return `
328
- <div class="stack stack-sm">
329
- ${decisions.map(d => `
330
- <div class="decision-item">
331
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-1)">
332
- <p style="font-size:var(--text-sm);font-weight:700">${esc(d.title)}</p>
333
- <span class="badge badge-muted">${esc(d.owner)}</span>
334
- </div>
335
- <p style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(d.impact || '')}</p>
336
- </div>
337
- `).join('')}
338
- </div>
339
- `;
340
- }