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.
Files changed (60) hide show
  1. package/README.md +307 -701
  2. package/bin/trackops.js +24 -16
  3. package/lib/config.js +265 -58
  4. package/lib/control.js +830 -292
  5. package/lib/init.js +46 -16
  6. package/lib/opera-bootstrap.js +85 -45
  7. package/lib/opera-phase-dod.js +485 -0
  8. package/lib/opera.js +8 -5
  9. package/lib/plans.js +1329 -0
  10. package/lib/quality-assert.js +49 -0
  11. package/lib/quality.js +1759 -0
  12. package/lib/release.js +18 -11
  13. package/lib/server.js +504 -192
  14. package/lib/skills.js +94 -41
  15. package/locales/en.json +249 -15
  16. package/locales/es.json +249 -15
  17. package/package.json +3 -2
  18. package/scripts/quality-unit-tests.js +130 -0
  19. package/scripts/skills-marketplace-smoke.js +156 -124
  20. package/scripts/smoke-tests.js +378 -71
  21. package/scripts/sync-skill-version.js +29 -19
  22. package/scripts/validate-skill.js +188 -103
  23. package/skills/trackops/SKILL.md +25 -7
  24. package/skills/trackops/locales/en/SKILL.md +25 -7
  25. package/skills/trackops/locales/en/references/activation.md +3 -3
  26. package/skills/trackops/locales/en/references/workflow.md +5 -4
  27. package/skills/trackops/references/activation.md +3 -3
  28. package/skills/trackops/references/workflow.md +5 -4
  29. package/skills/trackops/skill.json +29 -29
  30. package/skills/trackops-quality-guard/SKILL.md +78 -0
  31. package/skills/trackops-quality-guard/agents/openai.yaml +7 -0
  32. package/skills/trackops-quality-guard/locales/en/SKILL.md +78 -0
  33. package/skills/trackops-quality-guard/locales/en/references/commands.md +36 -0
  34. package/skills/trackops-quality-guard/locales/en/references/decision-policy.md +16 -0
  35. package/skills/trackops-quality-guard/locales/en/references/output-format.md +24 -0
  36. package/skills/trackops-quality-guard/references/commands.md +36 -0
  37. package/skills/trackops-quality-guard/references/decision-policy.md +16 -0
  38. package/skills/trackops-quality-guard/references/output-format.md +24 -0
  39. package/skills/trackops-quality-guard/skill.json +28 -0
  40. package/templates/skills/opera-skill/SKILL.md +12 -0
  41. package/templates/skills/opera-skill/locales/en/SKILL.md +12 -0
  42. package/templates/skills/trackops-quality-guard/SKILL.md +72 -0
  43. package/templates/skills/trackops-quality-guard/locales/en/SKILL.md +72 -0
  44. package/templates/skills/trackops-quality-guard/locales/en/references/commands.md +30 -0
  45. package/templates/skills/trackops-quality-guard/locales/en/references/decision-policy.md +14 -0
  46. package/templates/skills/trackops-quality-guard/locales/en/references/output-format.md +21 -0
  47. package/templates/skills/trackops-quality-guard/references/commands.md +30 -0
  48. package/templates/skills/trackops-quality-guard/references/decision-policy.md +14 -0
  49. package/templates/skills/trackops-quality-guard/references/output-format.md +21 -0
  50. package/ui/js/api.js +93 -26
  51. package/ui/js/app.js +13 -7
  52. package/ui/js/filters.js +49 -29
  53. package/ui/js/time-tracker.js +41 -28
  54. package/ui/js/views/board.js +22 -14
  55. package/ui/js/views/dashboard.js +206 -49
  56. package/ui/js/views/execution.js +7 -3
  57. package/ui/js/views/plans.js +284 -0
  58. package/ui/js/views/scrum.js +25 -13
  59. package/ui/js/views/sidebar.js +9 -8
  60. 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
- // ─────────────────────────────── SYNC ───────────────────────────────────────
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 tasksView from './views/tasks.js';
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.search) {
73
- const q = filters.search.toLowerCase();
74
- const haystack = `${task.title} ${task.id} ${task.summary || ''} ${task.stream || ''}`.toLowerCase();
75
- if (!haystack.includes(q)) return false;
76
- }
77
- return true;
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
- * @returns {string} HTML
105
- */
106
- export function renderBar(viewId, filters, options = {}) {
107
- const statusLabels = state.getStatusLabels?.() || {};
108
- const phases = options.phases || state.getPhases?.() || [];
109
- const statuses = options.statuses || ['pending', 'in_progress', 'in_review', 'blocked', 'completed', 'cancelled'];
110
- const priorities = options.priorities || ['P0', 'P1', 'P2', 'P3'];
111
- const streams = options.streams || [];
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.search) {
184
- chips.push({ key: 'search', label: `"${filters.search}"` });
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}">
@@ -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
- } catch (err) {
41
- // Fallback local si el backend no tiene el endpoint todavía
42
- const entry = {
43
- id: `local-${Date.now()}`,
44
- taskId,
45
- taskTitle,
46
- startedAt: new Date().toISOString(),
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();
@@ -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.blocker ? `<span class="badge badge-danger" title="${esc(task.blocker)}">${icon('alertTriangle', 10)} ${t('status.blocked', {}, 'Blocked')}</span>` : ''}
153
- </div>
154
- </article>
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
- flash(t('ui.board.movedSuccess', { status: newStatus }, `Task moved to ${newStatus}.`), 'success');
220
- window.dispatchEvent(new CustomEvent('ops:refresh'));
221
- } catch (err) {
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
  });