trackops 2.0.6 → 2.2.0
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 +307 -701
- package/bin/trackops.js +24 -16
- package/lib/config.js +265 -58
- package/lib/control.js +830 -292
- package/lib/init.js +46 -16
- package/lib/opera-bootstrap.js +85 -45
- package/lib/opera-phase-dod.js +485 -0
- package/lib/opera.js +8 -5
- package/lib/plans.js +1329 -0
- package/lib/quality-assert.js +49 -0
- package/lib/quality.js +1759 -0
- package/lib/release.js +18 -11
- package/lib/server.js +504 -192
- package/lib/skills.js +94 -41
- package/locales/en.json +249 -15
- package/locales/es.json +249 -15
- package/package.json +3 -2
- package/scripts/quality-unit-tests.js +130 -0
- package/scripts/skills-marketplace-smoke.js +156 -124
- package/scripts/smoke-tests.js +378 -71
- package/scripts/sync-skill-version.js +29 -19
- package/scripts/validate-skill.js +188 -103
- package/skills/trackops/SKILL.md +25 -7
- package/skills/trackops/locales/en/SKILL.md +25 -7
- package/skills/trackops/locales/en/references/activation.md +3 -3
- package/skills/trackops/locales/en/references/workflow.md +5 -4
- package/skills/trackops/references/activation.md +3 -3
- package/skills/trackops/references/workflow.md +5 -4
- package/skills/trackops/skill.json +29 -29
- package/skills/trackops-quality-guard/SKILL.md +78 -0
- package/skills/trackops-quality-guard/agents/openai.yaml +7 -0
- package/skills/trackops-quality-guard/locales/en/SKILL.md +78 -0
- package/skills/trackops-quality-guard/locales/en/references/commands.md +36 -0
- package/skills/trackops-quality-guard/locales/en/references/decision-policy.md +16 -0
- package/skills/trackops-quality-guard/locales/en/references/output-format.md +24 -0
- package/skills/trackops-quality-guard/references/commands.md +36 -0
- package/skills/trackops-quality-guard/references/decision-policy.md +16 -0
- package/skills/trackops-quality-guard/references/output-format.md +24 -0
- package/skills/trackops-quality-guard/skill.json +28 -0
- package/templates/skills/opera-skill/SKILL.md +12 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +12 -0
- package/templates/skills/trackops-quality-guard/SKILL.md +72 -0
- package/templates/skills/trackops-quality-guard/locales/en/SKILL.md +72 -0
- package/templates/skills/trackops-quality-guard/locales/en/references/commands.md +30 -0
- package/templates/skills/trackops-quality-guard/locales/en/references/decision-policy.md +14 -0
- package/templates/skills/trackops-quality-guard/locales/en/references/output-format.md +21 -0
- package/templates/skills/trackops-quality-guard/references/commands.md +30 -0
- package/templates/skills/trackops-quality-guard/references/decision-policy.md +14 -0
- package/templates/skills/trackops-quality-guard/references/output-format.md +21 -0
- package/ui/js/api.js +93 -26
- package/ui/js/app.js +13 -7
- package/ui/js/filters.js +49 -29
- package/ui/js/time-tracker.js +41 -28
- package/ui/js/views/board.js +22 -14
- package/ui/js/views/dashboard.js +206 -49
- package/ui/js/views/execution.js +7 -3
- package/ui/js/views/plans.js +284 -0
- package/ui/js/views/scrum.js +25 -13
- package/ui/js/views/sidebar.js +9 -8
- package/ui/js/views/tasks.js +238 -134
package/ui/js/api.js
CHANGED
|
@@ -142,12 +142,12 @@ export async function createTask(payload) {
|
|
|
142
142
|
* @param {string} taskId
|
|
143
143
|
* @param {Object} payload
|
|
144
144
|
*/
|
|
145
|
-
export async function updateTask(taskId, payload) {
|
|
146
|
-
return call(`/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
147
|
-
method: 'PUT',
|
|
148
|
-
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload }),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
145
|
+
export async function updateTask(taskId, payload, meta = {}) {
|
|
146
|
+
return call(`/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload, ...meta }),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Ejecuta una acción sobre una tarea (start, review, complete, block, pending, cancel)
|
|
@@ -155,14 +155,81 @@ export async function updateTask(taskId, payload) {
|
|
|
155
155
|
* @param {string} action
|
|
156
156
|
* @param {string} [note]
|
|
157
157
|
*/
|
|
158
|
-
export async function taskAction(taskId, action, note = '') {
|
|
159
|
-
return call(`/api/tasks/${encodeURIComponent(taskId)}/action`, {
|
|
160
|
-
method: 'POST',
|
|
161
|
-
body: JSON.stringify({ projectId: state.get('currentProjectId'), action, note }),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ───────────────────────────────
|
|
158
|
+
export async function taskAction(taskId, action, note = '', meta = {}) {
|
|
159
|
+
return call(`/api/tasks/${encodeURIComponent(taskId)}/action`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), action, note, ...meta }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────── PLANS ──────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export async function getPlans() {
|
|
168
|
+
return call('/api/plans');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function getPlan(sourceId) {
|
|
172
|
+
return call(`/api/plans/${encodeURIComponent(sourceId)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function scanPlans(scanPath = '') {
|
|
176
|
+
return call('/api/plans/scan', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), path: scanPath || undefined }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function importPlan(payload) {
|
|
183
|
+
return call('/api/plans/import', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function applyPlan(sourceId, payload = {}) {
|
|
190
|
+
return call(`/api/plans/${encodeURIComponent(sourceId)}/apply`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function unlinkPlan(sourceId, payload = {}) {
|
|
197
|
+
return call(`/api/plans/${encodeURIComponent(sourceId)}/unlink`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─────────────────────────────── QUALITY ────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export async function getQuality() {
|
|
206
|
+
return call('/api/quality');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function verifyQuality(payload = {}) {
|
|
210
|
+
return call('/api/quality/verify', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), ...payload }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function getPhaseReadiness(phase = 'current') {
|
|
217
|
+
return call(`/api/quality/phase-readiness?phase=${encodeURIComponent(phase)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function getReleaseReadiness() {
|
|
221
|
+
return call('/api/quality/release-readiness');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function getPromotionReadiness(target = 'production') {
|
|
225
|
+
return call(`/api/quality/promotion-readiness?target=${encodeURIComponent(target)}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function getQualityWaivers() {
|
|
229
|
+
return call('/api/quality/waivers');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────── SYNC ───────────────────────────────────────
|
|
166
233
|
|
|
167
234
|
/**
|
|
168
235
|
* Sincroniza los docs del proyecto (task_plan.md, progress.md, findings.md)
|
|
@@ -180,12 +247,12 @@ export async function syncDocs() {
|
|
|
180
247
|
* Ejecuta un comando en el shell del proyecto
|
|
181
248
|
* @param {string} command
|
|
182
249
|
*/
|
|
183
|
-
export async function runCommand(command) {
|
|
184
|
-
return call('/api/commands', {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
body: JSON.stringify({ projectId: state.get('currentProjectId'), command }),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
250
|
+
export async function runCommand(command, meta = {}) {
|
|
251
|
+
return call('/api/commands', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), command, ...meta }),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
189
256
|
|
|
190
257
|
/**
|
|
191
258
|
* Crea un EventSource para hacer streaming de salida de una sesión
|
|
@@ -203,12 +270,12 @@ export function streamSession(sessionId) {
|
|
|
203
270
|
* @param {string} taskId
|
|
204
271
|
* @param {string} taskTitle
|
|
205
272
|
*/
|
|
206
|
-
export async function startTimeEntry(taskId, taskTitle) {
|
|
207
|
-
return call('/api/time/start', {
|
|
208
|
-
method: 'POST',
|
|
209
|
-
body: JSON.stringify({ projectId: state.get('currentProjectId'), taskId, taskTitle }),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
273
|
+
export async function startTimeEntry(taskId, taskTitle, meta = {}) {
|
|
274
|
+
return call('/api/time/start', {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
body: JSON.stringify({ projectId: state.get('currentProjectId'), taskId, taskTitle, ...meta }),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
212
279
|
|
|
213
280
|
/**
|
|
214
281
|
* Detiene el time entry activo
|
package/ui/js/app.js
CHANGED
|
@@ -15,8 +15,9 @@ import * as keyboard from './keyboard.js';
|
|
|
15
15
|
// Vistas
|
|
16
16
|
import { render as renderSidebar } from './views/sidebar.js';
|
|
17
17
|
import { render as renderTopbar } from './views/topbar.js';
|
|
18
|
-
import * as dashboardView from './views/dashboard.js';
|
|
19
|
-
import * as
|
|
18
|
+
import * as dashboardView from './views/dashboard.js';
|
|
19
|
+
import * as plansView from './views/plans.js';
|
|
20
|
+
import * as tasksView from './views/tasks.js';
|
|
20
21
|
import * as executionView from './views/execution.js';
|
|
21
22
|
import * as projectsView from './views/projects.js';
|
|
22
23
|
import { render as renderTimeline } from './views/timeline.js';
|
|
@@ -37,11 +38,16 @@ async function init() {
|
|
|
37
38
|
setTimeout(() => dashboardView.bindEvents?.(), 50);
|
|
38
39
|
return html;
|
|
39
40
|
});
|
|
40
|
-
router.register('tasks', async () => {
|
|
41
|
-
const html = await tasksView.render();
|
|
42
|
-
setTimeout(() => tasksView.bindEvents?.(), 50);
|
|
43
|
-
return html;
|
|
44
|
-
});
|
|
41
|
+
router.register('tasks', async () => {
|
|
42
|
+
const html = await tasksView.render();
|
|
43
|
+
setTimeout(() => tasksView.bindEvents?.(), 50);
|
|
44
|
+
return html;
|
|
45
|
+
});
|
|
46
|
+
router.register('plans', async () => {
|
|
47
|
+
const html = await plansView.render();
|
|
48
|
+
setTimeout(() => plansView.bindEvents?.(), 50);
|
|
49
|
+
return html;
|
|
50
|
+
});
|
|
45
51
|
router.register('terminal', async () => {
|
|
46
52
|
const html = await executionView.render();
|
|
47
53
|
setTimeout(() => executionView.bindEvents(), 50);
|
package/ui/js/filters.js
CHANGED
|
@@ -11,7 +11,7 @@ import { t } from './i18n.js';
|
|
|
11
11
|
const STORAGE_PREFIX = 'ops-filters-';
|
|
12
12
|
|
|
13
13
|
/** Claves de filtro soportadas */
|
|
14
|
-
const FILTER_KEYS = ['status', 'priority', 'phase', 'stream', 'search'];
|
|
14
|
+
const FILTER_KEYS = ['status', 'priority', 'phase', 'stream', 'sourceId', 'rootId', 'search'];
|
|
15
15
|
|
|
16
16
|
// ─────────────────────────────── PERSISTENCE ─────────────────────────────────
|
|
17
17
|
|
|
@@ -67,15 +67,17 @@ export function apply(tasks, filters) {
|
|
|
67
67
|
return tasks.filter(task => {
|
|
68
68
|
if (filters.status && task.status !== filters.status) return false;
|
|
69
69
|
if (filters.priority && task.priority !== filters.priority) return false;
|
|
70
|
-
if (filters.phase && task.phase !== filters.phase) return false;
|
|
71
|
-
if (filters.stream && task.stream !== filters.stream) return false;
|
|
72
|
-
if (filters.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
if (filters.phase && task.phase !== filters.phase) return false;
|
|
71
|
+
if (filters.stream && task.stream !== filters.stream) return false;
|
|
72
|
+
if (filters.sourceId && (task.sourceId || task.origin?.sourceId || '') !== filters.sourceId) return false;
|
|
73
|
+
if (filters.rootId && (task.rootId || task.id) !== filters.rootId) return false;
|
|
74
|
+
if (filters.search) {
|
|
75
|
+
const q = filters.search.toLowerCase();
|
|
76
|
+
const haystack = `${task.title} ${task.id} ${task.summary || ''} ${task.stream || ''} ${task.sourceId || task.origin?.sourceId || ''} ${task.rootTitle || ''}`.toLowerCase();
|
|
77
|
+
if (!haystack.includes(q)) return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// ─────────────────────────────── CONTEO ──────────────────────────────────────
|
|
@@ -99,16 +101,20 @@ export function count(filters) {
|
|
|
99
101
|
* @param {Object} options — Opciones de renderizado
|
|
100
102
|
* @param {Array} [options.statuses] — Estados disponibles
|
|
101
103
|
* @param {Array} [options.priorities] — Prioridades disponibles
|
|
102
|
-
* @param {Array} [options.phases] — Fases disponibles
|
|
103
|
-
* @param {Array} [options.streams] — Streams disponibles
|
|
104
|
-
* @
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
104
|
+
* @param {Array} [options.phases] — Fases disponibles
|
|
105
|
+
* @param {Array} [options.streams] — Streams disponibles
|
|
106
|
+
* @param {Array} [options.sources] — Source ids disponibles
|
|
107
|
+
* @param {Array} [options.roots] — Root tasks disponibles
|
|
108
|
+
* @returns {string} HTML
|
|
109
|
+
*/
|
|
110
|
+
export function renderBar(viewId, filters, options = {}) {
|
|
111
|
+
const statusLabels = state.getStatusLabels?.() || {};
|
|
112
|
+
const phases = options.phases || state.getPhases?.() || [];
|
|
113
|
+
const statuses = options.statuses || ['pending', 'in_progress', 'in_review', 'blocked', 'completed', 'cancelled'];
|
|
114
|
+
const priorities = options.priorities || ['P0', 'P1', 'P2', 'P3'];
|
|
115
|
+
const streams = options.streams || [];
|
|
116
|
+
const sources = options.sources || [];
|
|
117
|
+
const roots = options.roots || [];
|
|
112
118
|
|
|
113
119
|
return `
|
|
114
120
|
<div class="filter-bar" role="search" aria-label="${t('ui.filters.label', {}, 'Filter tasks')}">
|
|
@@ -125,9 +131,17 @@ export function renderBar(viewId, filters, options = {}) {
|
|
|
125
131
|
value: p.id, label: `${p.id} · ${p.label}`
|
|
126
132
|
})), t('ui.filters.phase', {}, 'Phase')) : ''}
|
|
127
133
|
|
|
128
|
-
${streams.length ? _renderSelect('stream', filters.stream, streams.map(s => ({
|
|
129
|
-
value: s, label: s
|
|
130
|
-
})), t('ui.filters.stream', {}, 'Stream')) : ''}
|
|
134
|
+
${streams.length ? _renderSelect('stream', filters.stream, streams.map(s => ({
|
|
135
|
+
value: s, label: s
|
|
136
|
+
})), t('ui.filters.stream', {}, 'Stream')) : ''}
|
|
137
|
+
|
|
138
|
+
${sources.length ? _renderSelect('sourceId', filters.sourceId, sources.map(source => ({
|
|
139
|
+
value: source, label: source
|
|
140
|
+
})), t('ui.filters.source', {}, 'Source')) : ''}
|
|
141
|
+
|
|
142
|
+
${roots.length ? _renderSelect('rootId', filters.rootId, roots.map(root => ({
|
|
143
|
+
value: root.id, label: root.title
|
|
144
|
+
})), t('ui.filters.root', {}, 'Root')) : ''}
|
|
131
145
|
|
|
132
146
|
<div class="filter-search">
|
|
133
147
|
<input
|
|
@@ -177,12 +191,18 @@ function _renderActiveChips(filters, statusLabels, phases) {
|
|
|
177
191
|
const phase = phases.find(p => p.id === filters.phase);
|
|
178
192
|
chips.push({ key: 'phase', label: phase ? `${phase.id} · ${phase.label}` : filters.phase });
|
|
179
193
|
}
|
|
180
|
-
if (filters.stream) {
|
|
181
|
-
chips.push({ key: 'stream', label: filters.stream });
|
|
182
|
-
}
|
|
183
|
-
if (filters.
|
|
184
|
-
chips.push({ key: '
|
|
185
|
-
}
|
|
194
|
+
if (filters.stream) {
|
|
195
|
+
chips.push({ key: 'stream', label: filters.stream });
|
|
196
|
+
}
|
|
197
|
+
if (filters.sourceId) {
|
|
198
|
+
chips.push({ key: 'sourceId', label: filters.sourceId });
|
|
199
|
+
}
|
|
200
|
+
if (filters.rootId) {
|
|
201
|
+
chips.push({ key: 'rootId', label: filters.rootId });
|
|
202
|
+
}
|
|
203
|
+
if (filters.search) {
|
|
204
|
+
chips.push({ key: 'search', label: `"${filters.search}"` });
|
|
205
|
+
}
|
|
186
206
|
|
|
187
207
|
return chips.map(c => `
|
|
188
208
|
<span class="chip chip-active" data-filter-remove="${c.key}">
|
package/ui/js/time-tracker.js
CHANGED
|
@@ -18,32 +18,40 @@ let _startMs = null;
|
|
|
18
18
|
* @param {string} taskId
|
|
19
19
|
* @param {string} taskTitle
|
|
20
20
|
*/
|
|
21
|
-
export async function start(taskId, taskTitle) {
|
|
22
|
-
// Si hay uno en curso, detenerlo primero
|
|
23
|
-
if (state.get('activeEntry')) {
|
|
24
|
-
await stop();
|
|
25
|
-
}
|
|
21
|
+
export async function start(taskId, taskTitle) {
|
|
22
|
+
// Si hay uno en curso, detenerlo primero
|
|
23
|
+
if (state.get('activeEntry')) {
|
|
24
|
+
await stop();
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
try {
|
|
28
|
-
const result = await api.startTimeEntry(taskId, taskTitle
|
|
27
|
+
try {
|
|
28
|
+
const result = await api.startTimeEntry(taskId, taskTitle, {
|
|
29
|
+
actor: 'user',
|
|
30
|
+
source: 'time_tracker',
|
|
31
|
+
});
|
|
29
32
|
const entry = {
|
|
30
33
|
id: result.entry?.id || `local-${Date.now()}`,
|
|
31
34
|
taskId,
|
|
32
35
|
taskTitle,
|
|
33
36
|
startedAt: result.entry?.startedAt || new Date().toISOString(),
|
|
34
37
|
};
|
|
35
|
-
state.update('activeEntry', entry);
|
|
36
|
-
_startMs = Date.now() - (result.entry?.elapsedMs || 0);
|
|
37
|
-
_startInterval();
|
|
38
|
-
_updateTopbarTimer();
|
|
39
|
-
flash(`Timer iniciado: ${taskTitle}`, 'success');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
state.update('activeEntry', entry);
|
|
39
|
+
_startMs = Date.now() - (result.entry?.elapsedMs || 0);
|
|
40
|
+
_startInterval();
|
|
41
|
+
_updateTopbarTimer();
|
|
42
|
+
flash(`Timer iniciado: ${taskTitle}`, 'success');
|
|
43
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (!_shouldUseLocalFallback(err)) {
|
|
46
|
+
flash(err.message || 'No se pudo iniciar el timer.', 'error');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Fallback local si el backend no tiene el endpoint todavía
|
|
50
|
+
const entry = {
|
|
51
|
+
id: `local-${Date.now()}`,
|
|
52
|
+
taskId,
|
|
53
|
+
taskTitle,
|
|
54
|
+
startedAt: new Date().toISOString(),
|
|
47
55
|
};
|
|
48
56
|
state.update('activeEntry', entry);
|
|
49
57
|
_startMs = Date.now();
|
|
@@ -72,9 +80,9 @@ export function resume() {
|
|
|
72
80
|
/**
|
|
73
81
|
* Detiene el timer y persiste en el backend
|
|
74
82
|
*/
|
|
75
|
-
export async function stop() {
|
|
76
|
-
const entry = state.get('activeEntry');
|
|
77
|
-
if (!entry) return;
|
|
83
|
+
export async function stop() {
|
|
84
|
+
const entry = state.get('activeEntry');
|
|
85
|
+
if (!entry) return;
|
|
78
86
|
|
|
79
87
|
_stopInterval();
|
|
80
88
|
const elapsed = _startMs ? Date.now() - _startMs : 0;
|
|
@@ -99,10 +107,11 @@ export async function stop() {
|
|
|
99
107
|
state.update('timeEntries', timeEntries.slice(0, 50));
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
state.update('activeEntry', null);
|
|
103
|
-
_startMs = null;
|
|
104
|
-
_updateTopbarTimer();
|
|
105
|
-
|
|
110
|
+
state.update('activeEntry', null);
|
|
111
|
+
_startMs = null;
|
|
112
|
+
_updateTopbarTimer();
|
|
113
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
114
|
+
}
|
|
106
115
|
|
|
107
116
|
/**
|
|
108
117
|
* Obtiene el tiempo transcurrido actual en ms
|
|
@@ -144,13 +153,17 @@ function _startInterval() {
|
|
|
144
153
|
}, 1000);
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
function _stopInterval() {
|
|
156
|
+
function _stopInterval() {
|
|
148
157
|
if (_interval) {
|
|
149
158
|
clearInterval(_interval);
|
|
150
159
|
_interval = null;
|
|
151
160
|
}
|
|
152
161
|
state.update('timerInterval', null);
|
|
153
|
-
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _shouldUseLocalFallback(err) {
|
|
165
|
+
return err?.status === 404 || err?.status === 405 || err?.status === 501;
|
|
166
|
+
}
|
|
154
167
|
|
|
155
168
|
function _updateTimerDisplays() {
|
|
156
169
|
const elapsed = getElapsed();
|
package/ui/js/views/board.js
CHANGED
|
@@ -145,15 +145,20 @@ function _renderCard(task, phases) {
|
|
|
145
145
|
<strong class="task-card-title">${esc(task.title)}</strong>
|
|
146
146
|
<span class="task-card-id">${esc(task.id)}</span>
|
|
147
147
|
<p class="task-card-summary">${esc(task.summary || t('ui.board.noDescription', {}, 'No description.'))}</p>
|
|
148
|
-
<div class="task-card-meta">
|
|
149
|
-
<span class="badge badge-${priorityVariant[task.priority] || 'muted'}">${esc(task.priority)}</span>
|
|
150
|
-
<span class="badge badge-muted">${esc(phaseInfo?.label || task.phase)}</span>
|
|
151
|
-
${task.stream ? `<span class="badge badge-muted">${esc(task.stream)}</span>` : ''}
|
|
152
|
-
${task.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
148
|
+
<div class="task-card-meta">
|
|
149
|
+
<span class="badge badge-${priorityVariant[task.priority] || 'muted'}">${esc(task.priority)}</span>
|
|
150
|
+
<span class="badge badge-muted">${esc(phaseInfo?.label || task.phase)}</span>
|
|
151
|
+
${task.stream ? `<span class="badge badge-muted">${esc(task.stream)}</span>` : ''}
|
|
152
|
+
${task.executionOwner ? `<span class="badge badge-muted">${esc(`exec:${task.executionOwner}`)}</span>` : ''}
|
|
153
|
+
${task.sourceId ? `<span class="badge badge-muted">${esc(`plan:${task.sourceId}`)}</span>` : ''}
|
|
154
|
+
${task.rootId && task.rootId !== task.id ? `<span class="badge badge-muted">${esc(`root:${task.rootTitle}`)}</span>` : ''}
|
|
155
|
+
${task.awaitingUserConfirmation ? `<span class="badge badge-warning">${t('ui.tasks.awaitingUser', {}, 'Awaiting user')}</span>` : ''}
|
|
156
|
+
${task.verificationPending ? `<span class="badge badge-accent">${t('ui.tasks.verifyPending', {}, 'Verify state')}</span>` : ''}
|
|
157
|
+
${task.blocker ? `<span class="badge badge-danger" title="${esc(task.blocker)}">${icon('alertTriangle', 10)} ${t('status.blocked', {}, 'Blocked')}</span>` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
</article>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
157
162
|
|
|
158
163
|
function _bindDragDrop(board) {
|
|
159
164
|
board.addEventListener('dragstart', e => {
|
|
@@ -214,11 +219,14 @@ function _bindDragDrop(board) {
|
|
|
214
219
|
const action = statusToAction[newStatus];
|
|
215
220
|
if (!action) return;
|
|
216
221
|
|
|
217
|
-
try {
|
|
218
|
-
await api.taskAction(taskId, action, t('ui.board.movedFromBoard', { status: newStatus }, `Moved to ${newStatus} from the board.`)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
try {
|
|
223
|
+
await api.taskAction(taskId, action, t('ui.board.movedFromBoard', { status: newStatus }, `Moved to ${newStatus} from the board.`), {
|
|
224
|
+
actor: 'user',
|
|
225
|
+
source: 'board',
|
|
226
|
+
});
|
|
227
|
+
flash(t('ui.board.movedSuccess', { status: newStatus }, `Task moved to ${newStatus}.`), 'success');
|
|
228
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
229
|
+
} catch (err) {
|
|
222
230
|
flash(err.message, 'error');
|
|
223
231
|
}
|
|
224
232
|
});
|