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/views/dashboard.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { icon } from '../icons.js';
|
|
7
|
-
import * as state from '../state.js';
|
|
8
|
-
import * as api from '../api.js';
|
|
9
|
-
import { esc, formatDate, formatDurationShort, extractHistory, lastDays } from '../utils.js';
|
|
10
|
-
import { t } from '../i18n.js';
|
|
11
|
-
import { sparkline, lineChart, heatmap } from '../charts.js';
|
|
7
|
+
import * as state from '../state.js';
|
|
8
|
+
import * as api from '../api.js';
|
|
9
|
+
import { esc, formatDate, formatDurationShort, extractHistory, lastDays } from '../utils.js';
|
|
10
|
+
import { t } from '../i18n.js';
|
|
11
|
+
import { sparkline, lineChart, heatmap } from '../charts.js';
|
|
12
|
+
import { flash } from './flash.js';
|
|
12
13
|
|
|
13
14
|
/** Map OPERA phase IDs to standard dev labels */
|
|
14
15
|
const PHASE_LABELS = {
|
|
@@ -26,7 +27,7 @@ function _phaseLabel(phaseId) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// ─── Module state ────────────────────────────────────────────────
|
|
29
|
-
let _activeTab = 'overview'; // 'overview' | 'analytics'
|
|
30
|
+
let _activeTab = 'overview'; // 'overview' | 'analytics' | 'quality'
|
|
30
31
|
let _activePeriod = '30d'; // for analytics tab
|
|
31
32
|
let _analytics = null;
|
|
32
33
|
|
|
@@ -53,10 +54,11 @@ export async function render() {
|
|
|
53
54
|
<div class="section-header" style="display:flex;justify-content:space-between;align-items:center">
|
|
54
55
|
<h2 class="section-title">${t('ui.dashboard.title', {}, 'Dashboard')}</h2>
|
|
55
56
|
<div class="tab-pills" role="tablist" aria-label="${t('ui.dashboard.tabs', {}, 'Dashboard tabs')}">
|
|
56
|
-
<button class="tab-pill${_activeTab === 'overview' ? ' is-active' : ''}" data-tab="overview" role="tab" aria-selected="${_activeTab === 'overview'}" aria-controls="dashboard-content">${t('ui.dashboard.tabOverview', {}, 'Overview')}</button>
|
|
57
|
-
<button class="tab-pill${_activeTab === 'analytics' ? ' is-active' : ''}" data-tab="analytics" role="tab" aria-selected="${_activeTab === 'analytics'}" aria-controls="dashboard-content">${t('ui.dashboard.tabAnalytics', {}, 'Analytics')}</button>
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
<button class="tab-pill${_activeTab === 'overview' ? ' is-active' : ''}" data-tab="overview" role="tab" aria-selected="${_activeTab === 'overview'}" aria-controls="dashboard-content">${t('ui.dashboard.tabOverview', {}, 'Overview')}</button>
|
|
58
|
+
<button class="tab-pill${_activeTab === 'analytics' ? ' is-active' : ''}" data-tab="analytics" role="tab" aria-selected="${_activeTab === 'analytics'}" aria-controls="dashboard-content">${t('ui.dashboard.tabAnalytics', {}, 'Analytics')}</button>
|
|
59
|
+
<button class="tab-pill${_activeTab === 'quality' ? ' is-active' : ''}" data-tab="quality" role="tab" aria-selected="${_activeTab === 'quality'}" aria-controls="dashboard-content">${t('ui.dashboard.tabQuality', {}, 'Quality')}</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
60
62
|
<div id="dashboard-content" role="tabpanel">
|
|
61
63
|
${content}
|
|
62
64
|
</div>
|
|
@@ -100,22 +102,25 @@ function _bindTabSwitching() {
|
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
function _bindActiveTabEvents() {
|
|
104
|
-
if (_activeTab === 'overview') {
|
|
105
|
-
_bindTaskActionEvents();
|
|
106
|
-
} else {
|
|
107
|
-
_bindPeriodEvents();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
105
|
+
function _bindActiveTabEvents() {
|
|
106
|
+
if (_activeTab === 'overview') {
|
|
107
|
+
_bindTaskActionEvents();
|
|
108
|
+
} else if (_activeTab === 'analytics') {
|
|
109
|
+
_bindPeriodEvents();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
110
112
|
|
|
111
113
|
// ─── Tab content router ──────────────────────────────────────────
|
|
112
114
|
|
|
113
|
-
async function _renderTabContent(payload) {
|
|
114
|
-
if (_activeTab === '
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
async function _renderTabContent(payload) {
|
|
116
|
+
if (_activeTab === 'quality') {
|
|
117
|
+
return _renderQualityTab(payload);
|
|
118
|
+
}
|
|
119
|
+
if (_activeTab === 'analytics') {
|
|
120
|
+
return _renderAnalyticsTab(payload);
|
|
121
|
+
}
|
|
122
|
+
return _renderOverviewTab(payload);
|
|
123
|
+
}
|
|
119
124
|
|
|
120
125
|
// ═══════════════════════════════════════════════════════════════════
|
|
121
126
|
// OVERVIEW TAB
|
|
@@ -177,13 +182,18 @@ function _renderOverviewTab(payload) {
|
|
|
177
182
|
</div>
|
|
178
183
|
|
|
179
184
|
<!-- Next task -->
|
|
180
|
-
<div class="glass-card chart-card card-hover-lift stagger-4" aria-label="${t('ui.overview.nextTask', {}, 'Next task')}">
|
|
181
|
-
<p class="chart-title" style="margin-bottom:var(--space-3)">${t('ui.overview.nextMove', {}, 'Next move')}</p>
|
|
182
|
-
${derived.nextTask ? _renderNextTask(derived.nextTask) : `<p class="text-muted" style="font-size:var(--text-sm)">${t('ui.overview.noOpenTasks', {}, 'No open tasks')}</p>`}
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
<div class="glass-card chart-card card-hover-lift stagger-4" aria-label="${t('ui.overview.nextTask', {}, 'Next task')}">
|
|
186
|
+
<p class="chart-title" style="margin-bottom:var(--space-3)">${t('ui.overview.nextMove', {}, 'Next move')}</p>
|
|
187
|
+
${derived.nextTask ? _renderNextTask(derived.nextTask) : `<p class="text-muted" style="font-size:var(--text-sm)">${t('ui.overview.noOpenTasks', {}, 'No open tasks')}</p>`}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="glass-card chart-card card-hover-lift stagger-4" aria-label="${t('ui.overview.agentCoordination', {}, 'Agent coordination')}">
|
|
191
|
+
<p class="chart-title" style="margin-bottom:var(--space-3)">${t('ui.overview.agentCoordination', {}, 'Agent coordination')}</p>
|
|
192
|
+
${_renderCoordinationCard(derived, control)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
187
197
|
|
|
188
198
|
<!-- (Health grid moved to Analytics tab — no duplication) -->
|
|
189
199
|
</div>
|
|
@@ -356,7 +366,7 @@ function _renderDonut(totals) {
|
|
|
356
366
|
|
|
357
367
|
// ─────────────────────────────── NEXT TASK ──────────────────────────────────
|
|
358
368
|
|
|
359
|
-
function _renderNextTask(task) {
|
|
369
|
+
function _renderNextTask(task) {
|
|
360
370
|
const priorityColors = { P0: 'danger', P1: 'warning', P2: 'accent', P3: 'muted' };
|
|
361
371
|
const statusLabels = state.getStatusLabels();
|
|
362
372
|
const statusLabel = statusLabels[task.status] || task.status;
|
|
@@ -406,24 +416,171 @@ function _renderNextTask(task) {
|
|
|
406
416
|
|
|
407
417
|
// ─────────────────────────────── TASK ACTION EVENTS ─────────────────────────
|
|
408
418
|
|
|
409
|
-
function _bindTaskActionEvents() {
|
|
410
|
-
document.querySelectorAll('[data-task-action]').forEach(btn => {
|
|
411
|
-
btn.addEventListener('click', () => {
|
|
412
|
-
const taskId = btn.dataset.taskId;
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
419
|
+
function _bindTaskActionEvents() {
|
|
420
|
+
document.querySelectorAll('[data-task-action]').forEach(btn => {
|
|
421
|
+
btn.addEventListener('click', async () => {
|
|
422
|
+
const taskId = btn.dataset.taskId;
|
|
423
|
+
const action = btn.dataset.taskAction;
|
|
424
|
+
if (!taskId || !action) return;
|
|
425
|
+
try {
|
|
426
|
+
await api.taskAction(taskId, action, t('ui.overview.defaultActionNote', { action }, `Change to "${action}" from dashboard overview.`), {
|
|
427
|
+
actor: 'user',
|
|
428
|
+
source: 'dashboard_overview',
|
|
429
|
+
});
|
|
430
|
+
flash(t('ui.overview.actionApplied', {}, 'Task updated.'), 'success');
|
|
431
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
432
|
+
} catch (err) {
|
|
433
|
+
flash(err.message, 'error');
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function _renderCoordinationCard(derived, control) {
|
|
440
|
+
const inbox = control.meta?.agentInbox?.pending || [];
|
|
441
|
+
const awaitingUser = derived.awaitingUserTasks || [];
|
|
442
|
+
if (!inbox.length && !awaitingUser.length) {
|
|
443
|
+
return `<p class="text-muted" style="font-size:var(--text-sm)">${t('ui.overview.noCoordination', {}, 'No pending coordination between user and agent.')}</p>`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const lines = [];
|
|
447
|
+
awaitingUser.slice(0, 3).forEach(task => {
|
|
448
|
+
lines.push(`<div class="info-row"><p class="label-sm">${t('ui.overview.awaitingUser', {}, 'Awaiting user')}</p><p class="value">${esc(task.title)}</p></div>`);
|
|
449
|
+
});
|
|
450
|
+
inbox.slice(0, 4).forEach(item => {
|
|
451
|
+
lines.push(`<div class="info-row"><p class="label-sm">${esc(item.kind)}</p><p class="value">${esc(item.message)}</p></div>`);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return `<div class="stack stack-sm">${lines.join('')}</div>`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
458
|
+
// ANALYTICS TAB
|
|
459
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
460
|
+
|
|
461
|
+
function _renderQualityTab(payload) {
|
|
462
|
+
const quality = payload.quality || {};
|
|
463
|
+
const report = quality.report || { probes: [], summary: { counts: {} }, verification: { latest: {} } };
|
|
464
|
+
const phaseReadiness = quality.phaseReadiness || { status: 'unknown', blockers: [] };
|
|
465
|
+
const releaseReadiness = quality.releaseReadiness || { status: 'unknown', blockers: [] };
|
|
466
|
+
const promotionReadiness = quality.promotionReadiness || { status: 'unknown', blockers: [] };
|
|
467
|
+
const waivers = quality.waivers || [];
|
|
468
|
+
const failing = (report.probes || []).filter((probe) => probe.status === 'fail');
|
|
469
|
+
const latestRuns = report.verification?.latest || {};
|
|
470
|
+
const domains = Object.entries(report.summary?.byDomain || {});
|
|
471
|
+
const statusLabel = (value) => t(`quality.status.${String(value || 'unknown').toLowerCase()}`, {}, String(value || 'unknown'));
|
|
472
|
+
const blockersLabel = (items) => (items || []).map((blocker) => blocker.id).join(', ') || t('ui.dashboard.quality.noBlockers', {}, 'No blockers');
|
|
473
|
+
|
|
474
|
+
return `
|
|
475
|
+
<div class="view-enter">
|
|
476
|
+
<div class="grid-4" style="margin-bottom:var(--space-5)">
|
|
477
|
+
${_renderKPI(
|
|
478
|
+
t('ui.dashboard.quality.kpiQuality', {}, 'Quality'),
|
|
479
|
+
statusLabel(report.summary?.overallStatus || 'unknown'),
|
|
480
|
+
t('ui.dashboard.quality.failingProbes', { count: report.summary?.counts?.fail || 0 }, `${report.summary?.counts?.fail || 0} failing probes`),
|
|
481
|
+
'shield',
|
|
482
|
+
(report.summary?.counts?.fail || 0) ? 'danger' : 'success',
|
|
483
|
+
)}
|
|
484
|
+
${_renderKPI(
|
|
485
|
+
t('ui.dashboard.quality.kpiPhase', {}, 'Phase readiness'),
|
|
486
|
+
statusLabel(phaseReadiness.status || 'unknown'),
|
|
487
|
+
t('ui.dashboard.quality.blockerCount', { count: phaseReadiness.blockers?.length || 0 }, `${phaseReadiness.blockers?.length || 0} blockers`),
|
|
488
|
+
'play',
|
|
489
|
+
phaseReadiness.ready ? 'success' : 'warning',
|
|
490
|
+
)}
|
|
491
|
+
${_renderKPI(
|
|
492
|
+
t('ui.dashboard.quality.kpiRelease', {}, 'Release readiness'),
|
|
493
|
+
statusLabel(releaseReadiness.status || 'unknown'),
|
|
494
|
+
t('ui.dashboard.quality.blockerCount', { count: releaseReadiness.blockers?.length || 0 }, `${releaseReadiness.blockers?.length || 0} blockers`),
|
|
495
|
+
'checkCircle',
|
|
496
|
+
releaseReadiness.ready ? 'success' : 'danger',
|
|
497
|
+
)}
|
|
498
|
+
${_renderKPI(
|
|
499
|
+
t('ui.dashboard.quality.kpiPromotion', {}, 'Production promotion'),
|
|
500
|
+
statusLabel(promotionReadiness.status || 'unknown'),
|
|
501
|
+
t('ui.dashboard.quality.blockerCount', { count: promotionReadiness.blockers?.length || 0 }, `${promotionReadiness.blockers?.length || 0} blockers`),
|
|
502
|
+
'alertCircle',
|
|
503
|
+
promotionReadiness.ready ? 'success' : 'danger',
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-5)">
|
|
508
|
+
<div class="glass-card chart-card">
|
|
509
|
+
<p class="chart-title">${t('ui.dashboard.quality.domainStatus', {}, 'Domain status')}</p>
|
|
510
|
+
<div class="stack stack-sm" style="margin-top:var(--space-3)">
|
|
511
|
+
${domains.length ? domains.map(([domain, counts]) => `
|
|
512
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)">
|
|
513
|
+
<span class="label-sm">${esc(domain)}</span>
|
|
514
|
+
<span class="text-muted" style="font-size:var(--text-xs)">
|
|
515
|
+
${t('ui.dashboard.quality.domainCounts', {
|
|
516
|
+
pass: counts.pass || 0,
|
|
517
|
+
fail: counts.fail || 0,
|
|
518
|
+
skip: counts.skip || 0,
|
|
519
|
+
}, `pass ${counts.pass || 0} · fail ${counts.fail || 0} · skip ${counts.skip || 0}`)}
|
|
520
|
+
</span>
|
|
521
|
+
</div>
|
|
522
|
+
`).join('') : `<p class="text-muted">${t('ui.dashboard.quality.noDomainData', {}, 'No domain data.')}</p>`}
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<div class="glass-card chart-card">
|
|
527
|
+
<p class="chart-title">${t('ui.dashboard.quality.latestVerification', {}, 'Latest verification')}</p>
|
|
528
|
+
<div class="stack stack-sm" style="margin-top:var(--space-3)">
|
|
529
|
+
${Object.keys(latestRuns).length ? Object.entries(latestRuns).map(([scope, run]) => `
|
|
530
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)">
|
|
531
|
+
<span class="label-sm">${esc(t(`quality.scope.${scope}`, {}, scope))}</span>
|
|
532
|
+
<span class="badge ${run.status === 'passed' ? 'badge-success' : run.status === 'skipped' ? 'badge-warning' : 'badge-danger'}">${esc(statusLabel(run.status || 'unknown'))}</span>
|
|
533
|
+
</div>
|
|
534
|
+
`).join('') : `<p class="text-muted">${t('ui.dashboard.quality.noVerificationRuns', {}, 'No verification runs recorded.')}</p>`}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<div class="glass-card chart-card">
|
|
539
|
+
<p class="chart-title">${t('ui.dashboard.quality.topBlockers', {}, 'Top blockers')}</p>
|
|
540
|
+
<div class="stack stack-sm" style="margin-top:var(--space-3)">
|
|
541
|
+
${failing.length ? failing.slice(0, 8).map((probe) => `
|
|
542
|
+
<div class="finding-item severity-${esc((probe.severity || 'medium').toLowerCase())}">
|
|
543
|
+
<p style="font-size:var(--text-sm);font-weight:700">${esc(probe.id)}</p>
|
|
544
|
+
<p style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(probe.message || '')}</p>
|
|
545
|
+
</div>
|
|
546
|
+
`).join('') : `<p class="text-muted">${t('ui.dashboard.quality.noFailingProbes', {}, 'No failing probes.')}</p>`}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div class="glass-card chart-card">
|
|
551
|
+
<p class="chart-title">${t('ui.dashboard.quality.readinessDetail', {}, 'Readiness detail')}</p>
|
|
552
|
+
<div class="stack stack-sm" style="margin-top:var(--space-3)">
|
|
553
|
+
${[phaseReadiness, releaseReadiness, promotionReadiness].map((item) => `
|
|
554
|
+
<div>
|
|
555
|
+
<p style="font-size:var(--text-sm);font-weight:700">${esc(t(`ui.dashboard.quality.kind.${item.kind || 'readiness'}`, {}, item.kind || 'readiness'))} · ${esc(statusLabel(item.status || 'unknown'))}</p>
|
|
556
|
+
<p style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(blockersLabel(item.blockers || []))}</p>
|
|
557
|
+
</div>
|
|
558
|
+
`).join('')}
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="glass-card chart-card">
|
|
563
|
+
<p class="chart-title">${t('ui.dashboard.quality.activeWaivers', {}, 'Active waivers')}</p>
|
|
564
|
+
<div class="stack stack-sm" style="margin-top:var(--space-3)">
|
|
565
|
+
${waivers.length ? waivers.map((waiver) => `
|
|
566
|
+
<div>
|
|
567
|
+
<p style="font-size:var(--text-sm);font-weight:700">${esc(waiver.probeId || waiver.id)}</p>
|
|
568
|
+
<p style="font-size:var(--text-xs);color:var(--text-secondary)">
|
|
569
|
+
${esc(t('ui.dashboard.quality.waiverSummary', {
|
|
570
|
+
scope: waiver.scope || 'release',
|
|
571
|
+
approvedBy: waiver.approvedBy || 'unknown',
|
|
572
|
+
}, `${waiver.scope || 'release'} · ${waiver.approvedBy || 'unknown'}`))}
|
|
573
|
+
</p>
|
|
574
|
+
</div>
|
|
575
|
+
`).join('') : `<p class="text-muted">${t('ui.dashboard.quality.noWaivers', {}, 'No active waivers.')}</p>`}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function _renderAnalyticsTab(payload) {
|
|
427
584
|
const { derived, control, runtime } = payload;
|
|
428
585
|
const statusLabels = state.getStatusLabels();
|
|
429
586
|
const history = extractHistory(control.tasks).slice(0, 20);
|
package/ui/js/views/execution.js
CHANGED
|
@@ -176,9 +176,13 @@ async function _runCommand() {
|
|
|
176
176
|
const btn = document.getElementById('run-cmd-btn');
|
|
177
177
|
if (btn) { btn.disabled = true; btn.innerHTML = `${icon('spinner', 15)} ${t('ui.execution.running', {}, 'Running…')}`; }
|
|
178
178
|
|
|
179
|
-
try {
|
|
180
|
-
const
|
|
181
|
-
const
|
|
179
|
+
try {
|
|
180
|
+
const selectedTaskId = state.get('selectedTaskId');
|
|
181
|
+
const result = await api.runCommand(cmd, {
|
|
182
|
+
taskId: selectedTaskId || undefined,
|
|
183
|
+
source: 'execution_console',
|
|
184
|
+
});
|
|
185
|
+
const sessionId = result.session?.id || result.sessionId;
|
|
182
186
|
|
|
183
187
|
// Añadir sesión al estado
|
|
184
188
|
const sessions = state.get('sessions');
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as api from '../api.js';
|
|
2
|
+
import * as state from '../state.js';
|
|
3
|
+
import { icon } from '../icons.js';
|
|
4
|
+
import { esc, formatDate } from '../utils.js';
|
|
5
|
+
import { flash } from './flash.js';
|
|
6
|
+
import { t } from '../i18n.js';
|
|
7
|
+
|
|
8
|
+
let _selectedSourceId = null;
|
|
9
|
+
let _scanResults = [];
|
|
10
|
+
let _currentSource = null;
|
|
11
|
+
|
|
12
|
+
function renderSourceCard(source) {
|
|
13
|
+
const isActive = source.id === _selectedSourceId;
|
|
14
|
+
return `
|
|
15
|
+
<button class="panel ${isActive ? 'is-selected' : ''}" type="button" data-plan-source="${esc(source.id)}" style="text-align:left">
|
|
16
|
+
<div style="display:flex;justify-content:space-between;gap:var(--space-3);align-items:flex-start">
|
|
17
|
+
<div>
|
|
18
|
+
<p class="eyebrow">${esc(source.adapter || 'plan')}</p>
|
|
19
|
+
<h3 class="panel-title" style="margin:0">${esc(source.title || source.id)}</h3>
|
|
20
|
+
<p style="margin-top:var(--space-1);color:var(--text-secondary);font-size:var(--text-sm)">${esc(source.id)}</p>
|
|
21
|
+
</div>
|
|
22
|
+
<span class="badge badge-muted">${esc(source.status || 'previewed')}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-top:var(--space-3)">
|
|
25
|
+
<span class="badge badge-muted">Warnings: ${source.warnings || 0}</span>
|
|
26
|
+
<span class="badge ${(source.conflicts || 0) ? 'badge-danger' : 'badge-muted'}">Conflicts: ${source.conflicts || 0}</span>
|
|
27
|
+
<span class="badge badge-muted">Managed: ${source.managedTaskCount || 0}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div style="margin-top:var(--space-3);font-size:var(--text-xs);color:var(--text-secondary)">
|
|
30
|
+
<div>Preview: ${source.lastPreviewAt ? esc(formatDate(source.lastPreviewAt, 'date')) : 'n/a'}</div>
|
|
31
|
+
<div>Applied: ${source.lastApplyAt ? esc(formatDate(source.lastApplyAt, 'date')) : 'n/a'}</div>
|
|
32
|
+
</div>
|
|
33
|
+
</button>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderPreview(sourceDetails) {
|
|
38
|
+
if (!sourceDetails) {
|
|
39
|
+
return `<div class="empty-state">${t('ui.plans.selectSource', {}, 'Select a plan source to inspect its preview.')}</div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const preview = sourceDetails.preview || {};
|
|
43
|
+
const operations = preview.operations || [];
|
|
44
|
+
const warnings = preview.warnings || [];
|
|
45
|
+
|
|
46
|
+
return `
|
|
47
|
+
<div class="stack stack-md">
|
|
48
|
+
<div class="panel">
|
|
49
|
+
<div style="display:flex;justify-content:space-between;gap:var(--space-3);align-items:flex-start;flex-wrap:wrap">
|
|
50
|
+
<div>
|
|
51
|
+
<p class="eyebrow">${esc(sourceDetails.source?.adapter || 'plan')}</p>
|
|
52
|
+
<h3 class="panel-title" style="margin:0">${esc(sourceDetails.source?.title || sourceDetails.source?.id || 'Plan')}</h3>
|
|
53
|
+
<p style="margin-top:var(--space-1);color:var(--text-secondary);font-size:var(--text-sm)">${esc(sourceDetails.source?.path || '')}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
|
56
|
+
<button class="btn btn-primary btn-sm" type="button" data-plan-apply="${esc(sourceDetails.source?.id || '')}">${icon('check', 14)} ${t('ui.plans.apply', {}, 'Apply')}</button>
|
|
57
|
+
<button class="btn btn-ghost btn-sm" type="button" data-plan-unlink="${esc(sourceDetails.source?.id || '')}">${icon('link', 14)} ${t('ui.plans.unlink', {}, 'Unlink')}</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="grid-split">
|
|
63
|
+
<div class="panel">
|
|
64
|
+
<p class="eyebrow">${t('ui.plans.summary', {}, 'Summary')}</p>
|
|
65
|
+
<div class="stack stack-sm">
|
|
66
|
+
<div class="info-row"><p class="label-sm">Create</p><p class="value">${preview.summary?.create || 0}</p></div>
|
|
67
|
+
<div class="info-row"><p class="label-sm">Update</p><p class="value">${preview.summary?.update || 0}</p></div>
|
|
68
|
+
<div class="info-row"><p class="label-sm">Reparent</p><p class="value">${preview.summary?.reparent || 0}</p></div>
|
|
69
|
+
<div class="info-row"><p class="label-sm">Detach</p><p class="value">${preview.summary?.detach || 0}</p></div>
|
|
70
|
+
<div class="info-row"><p class="label-sm">Conflicts</p><p class="value">${preview.summary?.conflicts || 0}</p></div>
|
|
71
|
+
<div class="info-row"><p class="label-sm">Warnings</p><p class="value">${preview.summary?.warnings || 0}</p></div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="panel">
|
|
76
|
+
<p class="eyebrow">${t('ui.plans.warnings', {}, 'Warnings')}</p>
|
|
77
|
+
${(warnings.length
|
|
78
|
+
? warnings.map((warning) => `<p style="font-size:var(--text-sm);color:var(--text-secondary)">${esc(warning)}</p>`).join('')
|
|
79
|
+
: `<p style="font-size:var(--text-sm);color:var(--text-secondary)">No parser warnings.</p>`)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="panel">
|
|
84
|
+
<p class="eyebrow">${t('ui.plans.operations', {}, 'Preview operations')}</p>
|
|
85
|
+
<div class="stack stack-sm">
|
|
86
|
+
${operations.length
|
|
87
|
+
? operations.map((operation) => `
|
|
88
|
+
<div class="task-card" style="cursor:default">
|
|
89
|
+
<div style="display:flex;justify-content:space-between;gap:var(--space-3);align-items:flex-start;flex-wrap:wrap">
|
|
90
|
+
<div>
|
|
91
|
+
<strong class="task-card-title">${esc(operation.title || operation.taskId || operation.externalNodeId)}</strong>
|
|
92
|
+
<span class="task-card-id">${esc(operation.taskId || operation.externalNodeId || '')}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
|
95
|
+
<span class="badge ${(operation.type === 'conflict') ? 'badge-danger' : 'badge-muted'}">${esc(operation.type)}</span>
|
|
96
|
+
${operation.conflictFields?.length ? `<span class="badge badge-danger">${esc(operation.conflictFields.join(', '))}</span>` : ''}
|
|
97
|
+
${operation.updatedFields?.length ? `<span class="badge badge-muted">${esc(operation.updatedFields.join(', '))}</span>` : ''}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
`).join('')
|
|
102
|
+
: `<div class="empty-state">${t('ui.plans.noPreview', {}, 'No preview operations available.')}</div>`}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderScanResults() {
|
|
110
|
+
if (!_scanResults.length) return '';
|
|
111
|
+
return `
|
|
112
|
+
<div class="panel">
|
|
113
|
+
<p class="eyebrow">${t('ui.plans.scanResults', {}, 'Detected candidates')}</p>
|
|
114
|
+
<div class="stack stack-sm">
|
|
115
|
+
${_scanResults.map((item) => `
|
|
116
|
+
<div class="task-card" style="cursor:default">
|
|
117
|
+
<div style="display:flex;justify-content:space-between;gap:var(--space-3);align-items:flex-start;flex-wrap:wrap">
|
|
118
|
+
<div>
|
|
119
|
+
<strong class="task-card-title">${esc(item.title)}</strong>
|
|
120
|
+
<span class="task-card-id">${esc(item.path)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div style="display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap">
|
|
123
|
+
<span class="badge badge-muted">${esc(item.adapter)}</span>
|
|
124
|
+
<span class="badge badge-muted">${Math.round((item.confidence || 0) * 100)}%</span>
|
|
125
|
+
<button class="btn btn-ghost btn-sm" type="button" data-plan-import-path="${esc(item.path)}" data-plan-import-adapter="${esc(item.adapter)}">
|
|
126
|
+
${icon('download', 14)} ${t('ui.plans.import', {}, 'Import')}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
`).join('')}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function render() {
|
|
138
|
+
const payload = state.getPayload();
|
|
139
|
+
const sourcesResult = await api.getPlans().catch(() => ({ sources: payload?.control?.meta?.plans?.sources || [] }));
|
|
140
|
+
const sources = sourcesResult.sources || [];
|
|
141
|
+
|
|
142
|
+
if (!_selectedSourceId && sources.length) {
|
|
143
|
+
_selectedSourceId = sources[0].id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (_selectedSourceId) {
|
|
147
|
+
_currentSource = await api.getPlan(_selectedSourceId).catch(() => null);
|
|
148
|
+
} else {
|
|
149
|
+
_currentSource = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `
|
|
153
|
+
<section class="view-section view-enter">
|
|
154
|
+
<div class="section-header" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:var(--space-3)">
|
|
155
|
+
<div>
|
|
156
|
+
<p class="section-eyebrow">${esc(payload?.project?.name || 'TrackOps')}</p>
|
|
157
|
+
<h2 class="section-title">${t('ui.plans.title', {}, 'Plans')}</h2>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="grid-split">
|
|
162
|
+
<div class="stack stack-md">
|
|
163
|
+
<div class="panel">
|
|
164
|
+
<p class="eyebrow">${t('ui.plans.importWizard', {}, 'Import wizard')}</p>
|
|
165
|
+
<form id="plans-import-form" class="stack stack-sm">
|
|
166
|
+
<div class="field">
|
|
167
|
+
<label for="plan-file">${t('ui.plans.file', {}, 'Plan file')}</label>
|
|
168
|
+
<input id="plan-file" type="text" placeholder="docs/implementation-plan.md" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="field-row">
|
|
171
|
+
<div class="field">
|
|
172
|
+
<label for="plan-adapter">${t('ui.plans.adapter', {}, 'Adapter')}</label>
|
|
173
|
+
<select id="plan-adapter">
|
|
174
|
+
<option value="auto">auto</option>
|
|
175
|
+
<option value="canonical">canonical</option>
|
|
176
|
+
<option value="claude">claude</option>
|
|
177
|
+
<option value="codex">codex</option>
|
|
178
|
+
<option value="antigravity">antigravity</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="field">
|
|
182
|
+
<label for="plan-source-id">${t('ui.plans.sourceId', {}, 'Source id')}</label>
|
|
183
|
+
<input id="plan-source-id" type="text" placeholder="checkout-redesign-plan" />
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="form-actions" style="justify-content:flex-start">
|
|
187
|
+
<button class="btn btn-primary" type="submit">${icon('download', 14)} ${t('ui.plans.import', {}, 'Import')}</button>
|
|
188
|
+
<button class="btn btn-ghost" type="button" id="plans-scan-btn">${icon('search', 14)} ${t('ui.plans.scan', {}, 'Scan')}</button>
|
|
189
|
+
</div>
|
|
190
|
+
</form>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
${renderScanResults()}
|
|
194
|
+
|
|
195
|
+
<div class="stack stack-sm" id="plans-source-list">
|
|
196
|
+
${sources.length
|
|
197
|
+
? sources.map((source) => renderSourceCard(source)).join('')
|
|
198
|
+
: `<div class="empty-state">${t('ui.plans.empty', {}, 'No imported plans yet.')}</div>`}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div id="plans-preview-panel">
|
|
203
|
+
${renderPreview(_currentSource)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</section>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function bindEvents() {
|
|
211
|
+
document.querySelectorAll('[data-plan-source]').forEach((button) => {
|
|
212
|
+
button.addEventListener('click', async () => {
|
|
213
|
+
_selectedSourceId = button.dataset.planSource;
|
|
214
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
document.getElementById('plans-scan-btn')?.addEventListener('click', async () => {
|
|
219
|
+
try {
|
|
220
|
+
const result = await api.scanPlans();
|
|
221
|
+
_scanResults = result.candidates || [];
|
|
222
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
flash(error.message, 'error');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
document.querySelectorAll('[data-plan-import-path]').forEach((button) => {
|
|
229
|
+
button.addEventListener('click', async () => {
|
|
230
|
+
try {
|
|
231
|
+
await api.importPlan({ file: button.dataset.planImportPath, adapter: button.dataset.planImportAdapter || 'auto' });
|
|
232
|
+
_selectedSourceId = null;
|
|
233
|
+
_scanResults = [];
|
|
234
|
+
flash(t('ui.plans.imported', {}, 'Plan imported.'), 'success');
|
|
235
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
flash(error.message, 'error');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
document.getElementById('plans-import-form')?.addEventListener('submit', async (event) => {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
const file = document.getElementById('plan-file')?.value.trim();
|
|
245
|
+
const adapter = document.getElementById('plan-adapter')?.value || 'auto';
|
|
246
|
+
const sourceId = document.getElementById('plan-source-id')?.value.trim();
|
|
247
|
+
if (!file) {
|
|
248
|
+
flash(t('ui.plans.fileRequired', {}, 'Plan file is required.'), 'error');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const result = await api.importPlan({ file, adapter, sourceId: sourceId || undefined });
|
|
253
|
+
_selectedSourceId = result.source?.id || _selectedSourceId;
|
|
254
|
+
_scanResults = [];
|
|
255
|
+
flash(t('ui.plans.imported', {}, 'Plan imported.'), 'success');
|
|
256
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
257
|
+
} catch (error) {
|
|
258
|
+
flash(error.message, 'error');
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
document.querySelector('[data-plan-apply]')?.addEventListener('click', async (event) => {
|
|
263
|
+
const sourceId = event.currentTarget.dataset.planApply;
|
|
264
|
+
try {
|
|
265
|
+
await api.applyPlan(sourceId, { conflicts: 'abort', removed: 'detach' });
|
|
266
|
+
flash(t('ui.plans.applied', {}, 'Plan applied.'), 'success');
|
|
267
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
flash(error.message, 'error');
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
document.querySelector('[data-plan-unlink]')?.addEventListener('click', async (event) => {
|
|
274
|
+
const sourceId = event.currentTarget.dataset.planUnlink;
|
|
275
|
+
try {
|
|
276
|
+
await api.unlinkPlan(sourceId, { keepTasks: true });
|
|
277
|
+
_selectedSourceId = null;
|
|
278
|
+
flash(t('ui.plans.unlinked', {}, 'Plan unlinked.'), 'success');
|
|
279
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
flash(error.message, 'error');
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|