trackops 2.0.3 → 2.0.5
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/LICENSE +21 -21
- package/README.md +695 -402
- package/bin/trackops.js +116 -116
- package/lib/config.js +326 -326
- package/lib/control.js +208 -208
- package/lib/env.js +244 -244
- package/lib/init.js +325 -325
- package/lib/locale.js +24 -0
- package/lib/opera-bootstrap.js +941 -874
- package/lib/opera.js +494 -477
- package/lib/preferences.js +74 -74
- package/lib/registry.js +214 -196
- package/lib/release.js +56 -56
- package/lib/runtime-state.js +144 -144
- package/lib/server.js +312 -207
- package/lib/skills.js +74 -57
- package/lib/workspace.js +260 -260
- package/locales/en.json +192 -166
- package/locales/es.json +192 -166
- package/package.json +61 -58
- package/scripts/postinstall-locale.js +21 -21
- package/scripts/skills-marketplace-smoke.js +124 -124
- package/scripts/smoke-tests.js +558 -554
- package/scripts/sync-skill-version.js +21 -21
- package/scripts/validate-skill.js +103 -103
- package/skills/trackops/SKILL.md +126 -122
- package/skills/trackops/agents/openai.yaml +7 -7
- package/skills/trackops/locales/en/SKILL.md +126 -122
- package/skills/trackops/locales/en/references/activation.md +94 -75
- package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
- package/skills/trackops/locales/en/references/workflow.md +55 -32
- package/skills/trackops/references/activation.md +94 -75
- package/skills/trackops/references/troubleshooting.md +73 -55
- package/skills/trackops/references/workflow.md +55 -32
- package/skills/trackops/skill.json +29 -29
- package/templates/hooks/post-checkout +2 -2
- package/templates/hooks/post-commit +2 -2
- package/templates/hooks/post-merge +2 -2
- package/templates/opera/agent.md +28 -27
- package/templates/opera/architecture/dependency-graph.md +24 -24
- package/templates/opera/architecture/runtime-automation.md +24 -24
- package/templates/opera/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/agent.md +22 -21
- package/templates/opera/en/architecture/dependency-graph.md +24 -24
- package/templates/opera/en/architecture/runtime-automation.md +24 -24
- package/templates/opera/en/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/reviews/delivery-audit.md +18 -18
- package/templates/opera/en/reviews/integration-audit.md +18 -18
- package/templates/opera/en/router.md +24 -19
- package/templates/opera/references/autonomy-and-recovery.md +117 -117
- package/templates/opera/references/opera-cycle.md +193 -193
- package/templates/opera/registry.md +28 -28
- package/templates/opera/reviews/delivery-audit.md +18 -18
- package/templates/opera/reviews/integration-audit.md +18 -18
- package/templates/opera/router.md +54 -49
- package/templates/skills/changelog-updater/SKILL.md +69 -69
- package/templates/skills/commiter/SKILL.md +99 -99
- package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
- package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
- package/templates/skills/opera-policy-guard/SKILL.md +26 -26
- package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
- package/templates/skills/opera-skill/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
- package/templates/skills/opera-skill/references/phase-dod.md +138 -0
- package/templates/skills/project-starter-skill/SKILL.md +150 -131
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
- package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
- package/ui/css/base.css +284 -266
- package/ui/css/charts.css +425 -327
- package/ui/css/components.css +1107 -570
- package/ui/css/onboarding.css +133 -0
- package/ui/css/panels.css +345 -406
- package/ui/css/terminal.css +125 -0
- package/ui/css/timeline.css +58 -0
- package/ui/css/tokens.css +284 -227
- package/ui/favicon.svg +5 -5
- package/ui/index.html +99 -96
- package/ui/js/api.js +49 -13
- package/ui/js/app.js +28 -32
- package/ui/js/charts.js +526 -0
- package/ui/js/console-logger.js +172 -172
- package/ui/js/filters.js +247 -0
- package/ui/js/icons.js +129 -104
- package/ui/js/keyboard.js +229 -0
- package/ui/js/onboarding.js +33 -42
- package/ui/js/router.js +142 -125
- package/ui/js/theme.js +100 -100
- package/ui/js/time-tracker.js +248 -248
- package/ui/js/views/board.js +84 -114
- package/ui/js/views/dashboard.js +870 -0
- package/ui/js/views/flash.js +47 -47
- package/ui/js/views/projects.js +745 -0
- package/ui/js/views/scrum.js +476 -0
- package/ui/js/views/settings.js +153 -203
- package/ui/js/views/sidebar.js +37 -31
- package/ui/js/views/tasks.js +218 -101
- package/ui/js/views/timeline.js +265 -0
- package/ui/js/views/topbar.js +94 -107
- package/ui/app.js +0 -950
- package/ui/js/views/insights.js +0 -340
- package/ui/js/views/overview.js +0 -369
- package/ui/styles.css +0 -688
package/ui/js/views/board.js
CHANGED
|
@@ -1,97 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* board.js —
|
|
2
|
+
* board.js — Headless Kanban module (embeddable inside tasks.js)
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* renderKanban(tasks, phases, opts) — returns kanban board HTML (columns + cards only)
|
|
6
|
+
* bindKanbanEvents() — binds drag-drop, card click/selection, task actions
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import { icon } from '../icons.js';
|
|
6
10
|
import * as state from '../state.js';
|
|
7
11
|
import * as api from '../api.js';
|
|
8
|
-
import * as router from '../router.js';
|
|
9
12
|
import { flash } from './flash.js';
|
|
10
|
-
import { esc
|
|
13
|
+
import { esc } from '../utils.js';
|
|
11
14
|
import { t } from '../i18n.js';
|
|
12
15
|
|
|
13
16
|
const COLUMNS = [
|
|
14
17
|
{ id: 'pending', label: 'Pending' },
|
|
15
18
|
{ id: 'in_progress', label: 'In Progress' },
|
|
16
|
-
{ id: 'in_review',
|
|
17
|
-
{ id: 'blocked',
|
|
18
|
-
{ id: 'completed',
|
|
19
|
+
{ id: 'in_review', label: 'In Review' },
|
|
20
|
+
{ id: 'blocked', label: 'Blocked' },
|
|
21
|
+
{ id: 'completed', label: 'Completed' },
|
|
19
22
|
];
|
|
20
23
|
|
|
21
24
|
let _dragTaskId = null;
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Render the kanban board HTML (columns + cards only).
|
|
28
|
+
*
|
|
29
|
+
* @param {Array} tasks — pre-filtered task list
|
|
30
|
+
* @param {Array} phases — array of phase objects ({ id, label, ... })
|
|
31
|
+
* @param {Object} opts — { showCompleted, showCancelled, wipLimits }
|
|
32
|
+
* wipLimits is an optional map: { status: number }
|
|
33
|
+
* @returns {string} HTML string for the board grid
|
|
34
|
+
*/
|
|
35
|
+
export function renderKanban(tasks, phases, opts = {}) {
|
|
36
|
+
const { showCompleted = true, showCancelled = false, wipLimits = {} } = opts;
|
|
37
|
+
|
|
38
|
+
// Filter out completed / cancelled unless opted-in
|
|
39
|
+
let visibleTasks = tasks;
|
|
40
|
+
if (!showCompleted) {
|
|
41
|
+
visibleTasks = visibleTasks.filter(t => t.status !== 'completed');
|
|
42
|
+
}
|
|
43
|
+
if (!showCancelled) {
|
|
44
|
+
visibleTasks = visibleTasks.filter(t => t.status !== 'cancelled');
|
|
45
|
+
}
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
const hasCancelled =
|
|
47
|
+
// Build column set
|
|
48
|
+
const hasCancelled = showCancelled && visibleTasks.some(t => t.status === 'cancelled');
|
|
29
49
|
const columns = hasCancelled
|
|
30
50
|
? [...COLUMNS, { id: 'cancelled', label: t('status.cancelled', {}, 'Cancelled') }]
|
|
31
51
|
: COLUMNS.map(col => ({ ...col, label: t(`status.${col.id}`, {}, col.label) }));
|
|
32
52
|
|
|
33
|
-
|
|
34
|
-
<div class="
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<div style="display:flex;gap:var(--space-2)">
|
|
41
|
-
<button class="btn btn-ghost btn-sm" id="board-filter-done" type="button">
|
|
42
|
-
${icon('check', 14)} ${_showCompleted() ? t('ui.board.hideCompleted', {}, 'Hide completed') : t('ui.board.showCompleted', {}, 'Show completed')}
|
|
43
|
-
</button>
|
|
44
|
-
<button class="btn btn-primary btn-sm" id="new-task-btn" type="button" aria-label="${t('ui.board.newTask', {}, 'Create new task')}">
|
|
45
|
-
${icon('plus', 14)} ${t('ui.tasks.new', {}, 'New task')}
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div class="board-grid" id="board" aria-label="${t('ui.board.aria', {}, 'Task board by status')}" role="region">
|
|
51
|
-
${columns.map(col => {
|
|
52
|
-
const colTasks = tasks.filter(t => t.status === col.id);
|
|
53
|
-
return _renderColumn(col, colTasks);
|
|
54
|
-
}).join('')}
|
|
55
|
-
</div>
|
|
53
|
+
return `
|
|
54
|
+
<div class="board-grid" id="board" aria-label="${t('ui.board.aria', {}, 'Task board by status')}" role="region">
|
|
55
|
+
${columns.map(col => {
|
|
56
|
+
const colTasks = visibleTasks.filter(t => t.status === col.id);
|
|
57
|
+
const wipLimit = wipLimits[col.id] ?? null;
|
|
58
|
+
return _renderColumn(col, colTasks, phases, wipLimit);
|
|
59
|
+
}).join('')}
|
|
56
60
|
</div>
|
|
57
61
|
`;
|
|
58
|
-
|
|
59
|
-
setTimeout(() => _bindEvents(), 0);
|
|
60
|
-
return html;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Bind kanban-specific events: drag-drop, card click/selection, task actions.
|
|
66
|
+
* Call this after the board HTML has been inserted into the DOM.
|
|
67
|
+
*/
|
|
68
|
+
export function bindKanbanEvents() {
|
|
69
|
+
const board = document.getElementById('board');
|
|
70
|
+
if (!board) return;
|
|
71
|
+
|
|
72
|
+
// Click on task card — select and highlight
|
|
73
|
+
board.addEventListener('click', e => {
|
|
74
|
+
const card = e.target.closest('.task-card');
|
|
75
|
+
if (!card) return;
|
|
76
|
+
|
|
77
|
+
const id = card.dataset.taskId;
|
|
78
|
+
state.update('selectedTaskId', id);
|
|
79
|
+
|
|
80
|
+
// Highlight the selected card without a full re-render
|
|
81
|
+
board.querySelectorAll('.task-card').forEach(c => {
|
|
82
|
+
c.classList.toggle('is-selected', c.dataset.taskId === id);
|
|
83
|
+
c.setAttribute('aria-selected', c.dataset.taskId === id ? 'true' : 'false');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Keyboard on cards (Enter = select)
|
|
88
|
+
board.addEventListener('keydown', e => {
|
|
89
|
+
const card = e.target.closest('.task-card');
|
|
90
|
+
if (!card) return;
|
|
91
|
+
if (e.key === 'Enter') {
|
|
92
|
+
state.update('selectedTaskId', card.dataset.taskId);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
// Drag & drop
|
|
97
|
+
_bindDragDrop(board);
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
/* ------------------------------------------------------------------ */
|
|
101
|
+
/* Internal helpers */
|
|
102
|
+
/* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
function _renderColumn(col, tasks, phases, wipLimit) {
|
|
105
|
+
const wipExceeded = wipLimit != null && tasks.length > wipLimit;
|
|
106
|
+
const wipAttr = wipLimit != null ? ` data-wip-limit="${wipLimit}"` : '';
|
|
107
|
+
|
|
84
108
|
return `
|
|
85
|
-
<section class="board-column col-${col.id}" data-status="${col.id}" aria-label="${t('ui.board.column', { label: col.label }, `Column ${col.label}`)}">
|
|
86
|
-
<div class="board-column-header">
|
|
109
|
+
<section class="board-column col-${col.id}" data-status="${col.id}"${wipAttr} aria-label="${t('ui.board.column', { label: col.label }, `Column ${col.label}`)}">
|
|
110
|
+
<div class="board-column-header${wipExceeded ? ' wip-exceeded' : ''}">
|
|
87
111
|
<h3 class="board-column-title" id="col-${col.id}">
|
|
88
112
|
<span class="board-column-dot" aria-hidden="true"></span>
|
|
89
113
|
${esc(col.label)}
|
|
90
114
|
</h3>
|
|
91
|
-
<span class="board-column-count" aria-label="${t('ui.board.tasksCount', { count: tasks.length }, `${tasks.length} tasks`)}">${tasks.length}</span>
|
|
115
|
+
<span class="board-column-count" aria-label="${t('ui.board.tasksCount', { count: tasks.length }, `${tasks.length} tasks`)}">${tasks.length}${wipLimit != null ? `/${wipLimit}` : ''}</span>
|
|
92
116
|
</div>
|
|
93
117
|
<div class="board-column-body" aria-labelledby="col-${col.id}" role="list">
|
|
94
|
-
${tasks.map(
|
|
118
|
+
${tasks.map(task => _renderCard(task, phases)).join('')}
|
|
95
119
|
${tasks.length === 0
|
|
96
120
|
? `<div class="empty-state" style="padding:var(--space-5);min-height:80px;border-style:dashed">${t('ui.board.noTasks', {}, 'No tasks')}</div>`
|
|
97
121
|
: ''}
|
|
@@ -100,10 +124,9 @@ function _renderColumn(col, tasks) {
|
|
|
100
124
|
`;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
function _renderCard(task) {
|
|
127
|
+
function _renderCard(task, phases) {
|
|
104
128
|
const isSelected = task.id === state.get('selectedTaskId');
|
|
105
129
|
const statusLabels = state.getStatusLabels();
|
|
106
|
-
const phases = state.getPhases();
|
|
107
130
|
const phaseInfo = phases.find(p => p.id === task.phase);
|
|
108
131
|
|
|
109
132
|
const priorityVariant = { P0: 'danger', P1: 'warning', P2: 'accent', P3: 'muted' };
|
|
@@ -132,59 +155,7 @@ function _renderCard(task) {
|
|
|
132
155
|
`;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
function _bindEvents() {
|
|
136
|
-
const board = document.getElementById('board');
|
|
137
|
-
if (!board) return;
|
|
138
|
-
|
|
139
|
-
// Clic en task card → seleccionar y navegar a tasks
|
|
140
|
-
board.addEventListener('click', e => {
|
|
141
|
-
const card = e.target.closest('.task-card');
|
|
142
|
-
if (!card) return;
|
|
143
|
-
|
|
144
|
-
const id = card.dataset.taskId;
|
|
145
|
-
state.update('selectedTaskId', id);
|
|
146
|
-
|
|
147
|
-
// Navegar al editor si hubo doble clic; si un clic simple, solo marcar
|
|
148
|
-
if (e.detail === 2) {
|
|
149
|
-
router.navigate('tasks');
|
|
150
|
-
} else {
|
|
151
|
-
// Resaltar la card seleccionada sin re-render completo
|
|
152
|
-
board.querySelectorAll('.task-card').forEach(c => {
|
|
153
|
-
c.classList.toggle('is-selected', c.dataset.taskId === id);
|
|
154
|
-
c.setAttribute('aria-selected', c.dataset.taskId === id ? 'true' : 'false');
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Teclado en cards (Enter = seleccionar, Space = toggle)
|
|
160
|
-
board.addEventListener('keydown', e => {
|
|
161
|
-
const card = e.target.closest('.task-card');
|
|
162
|
-
if (!card) return;
|
|
163
|
-
if (e.key === 'Enter') {
|
|
164
|
-
state.update('selectedTaskId', card.dataset.taskId);
|
|
165
|
-
router.navigate('tasks');
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Toggle mostrar completadas
|
|
170
|
-
document.getElementById('board-filter-done')?.addEventListener('click', () => {
|
|
171
|
-
const current = _showCompleted();
|
|
172
|
-
sessionStorage.setItem('board-show-completed', current ? 'false' : 'true');
|
|
173
|
-
router.refresh();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Nueva tarea
|
|
177
|
-
document.getElementById('new-task-btn')?.addEventListener('click', () => {
|
|
178
|
-
state.update('selectedTaskId', null);
|
|
179
|
-
router.navigate('tasks');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Drag & drop
|
|
183
|
-
_bindDragDrop(board);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
158
|
function _bindDragDrop(board) {
|
|
187
|
-
// Dragstart
|
|
188
159
|
board.addEventListener('dragstart', e => {
|
|
189
160
|
const card = e.target.closest('.task-card');
|
|
190
161
|
if (!card) return;
|
|
@@ -213,7 +184,6 @@ function _bindDragDrop(board) {
|
|
|
213
184
|
board.addEventListener('dragleave', e => {
|
|
214
185
|
const col = e.target.closest('.board-column');
|
|
215
186
|
if (!col) return;
|
|
216
|
-
// Solo eliminar si salimos de la columna, no de un hijo
|
|
217
187
|
if (!col.contains(e.relatedTarget)) {
|
|
218
188
|
col.classList.remove('is-drop-target');
|
|
219
189
|
}
|