trackops 1.0.0 → 1.1.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 +341 -232
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +518 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +146 -55
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +912 -418
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +14 -3
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/etapa/agent.md +2 -2
- package/templates/etapa/references/etapa-cycle.md +1 -1
- package/templates/opera/agent.md +1 -1
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/SKILL.md +5 -3
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/base.css +266 -0
- package/ui/css/charts.css +327 -0
- package/ui/css/components.css +570 -0
- package/ui/css/panels.css +956 -0
- package/ui/css/tokens.css +227 -0
- package/ui/favicon.svg +5 -0
- package/ui/index.html +91 -351
- package/ui/js/api.js +220 -0
- package/ui/js/app.js +200 -0
- package/ui/js/console-logger.js +172 -0
- package/ui/js/i18n.js +14 -0
- package/ui/js/icons.js +104 -0
- package/ui/js/onboarding.js +439 -0
- package/ui/js/router.js +125 -0
- package/ui/js/state.js +130 -0
- package/ui/js/theme.js +100 -0
- package/ui/js/time-tracker.js +248 -0
- package/ui/js/utils.js +175 -0
- package/ui/js/views/board.js +255 -0
- package/ui/js/views/execution.js +256 -0
- package/ui/js/views/flash.js +47 -0
- package/ui/js/views/insights.js +340 -0
- package/ui/js/views/overview.js +365 -0
- package/ui/js/views/settings.js +381 -0
- package/ui/js/views/sidebar.js +131 -0
- package/ui/js/views/skills.js +163 -0
- package/ui/js/views/tasks.js +406 -0
- package/ui/js/views/topbar.js +239 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* topbar.js — Barra superior del dashboard
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { icon } from '../icons.js';
|
|
6
|
+
import * as state from '../state.js';
|
|
7
|
+
import * as api from '../api.js';
|
|
8
|
+
import { flash } from './flash.js';
|
|
9
|
+
import { esc, debounce } from '../utils.js';
|
|
10
|
+
import * as theme from '../theme.js';
|
|
11
|
+
import { t } from '../i18n.js';
|
|
12
|
+
|
|
13
|
+
/** Renderiza el topbar */
|
|
14
|
+
export function render() {
|
|
15
|
+
const el = document.getElementById('topbar');
|
|
16
|
+
if (!el) return;
|
|
17
|
+
|
|
18
|
+
const payload = state.getPayload();
|
|
19
|
+
const projects = state.get('projects');
|
|
20
|
+
const currentId = state.get('currentProjectId');
|
|
21
|
+
const currentLocale = state.get('locale') || payload?.i18n?.locale || 'es';
|
|
22
|
+
const runtime = payload?.runtime;
|
|
23
|
+
|
|
24
|
+
el.innerHTML = `
|
|
25
|
+
<div class="topbar">
|
|
26
|
+
<!-- Hamburger (mobile) -->
|
|
27
|
+
<button class="topbar-hamburger" type="button" id="sidebar-toggle" aria-label="${t('ui.topbar.openMenu', {}, 'Open menu')}" aria-expanded="false" aria-controls="sidebar">
|
|
28
|
+
<span></span><span></span><span></span>
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<!-- Search -->
|
|
32
|
+
<div class="topbar-search">
|
|
33
|
+
<div class="search-wrapper" role="search">
|
|
34
|
+
<span class="search-icon" aria-hidden="true">${icon('search', 16)}</span>
|
|
35
|
+
<input
|
|
36
|
+
type="search"
|
|
37
|
+
id="global-search"
|
|
38
|
+
aria-label="${t('ui.topbar.searchAria', {}, 'Search tasks')}"
|
|
39
|
+
placeholder="${t('ui.topbar.searchPlaceholder', {}, 'Search tasks…')}"
|
|
40
|
+
autocomplete="off"
|
|
41
|
+
value="${esc(state.get('searchQuery'))}"
|
|
42
|
+
/>
|
|
43
|
+
<span class="search-kbd" aria-hidden="true">⌘F</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Derecha -->
|
|
48
|
+
<div class="topbar-right">
|
|
49
|
+
|
|
50
|
+
<!-- Timer activo -->
|
|
51
|
+
<div class="topbar-timer" id="topbar-timer" aria-label="${t('ui.topbar.timer', {}, 'Time tracking')}" aria-live="polite">
|
|
52
|
+
<span class="topbar-timer-dot" aria-hidden="true"></span>
|
|
53
|
+
<span id="topbar-timer-display">00:00:00</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Repo status -->
|
|
57
|
+
${runtime ? _renderRepoBadge(runtime) : ''}
|
|
58
|
+
|
|
59
|
+
<!-- Project selector -->
|
|
60
|
+
${projects.length > 0 ? _renderProjectSelector(projects, currentId) : ''}
|
|
61
|
+
|
|
62
|
+
<!-- Locale selector -->
|
|
63
|
+
${_renderLocaleSelector(currentLocale)}
|
|
64
|
+
|
|
65
|
+
<!-- Sync button -->
|
|
66
|
+
<button class="btn btn-ghost btn-sm" id="sync-btn" type="button" aria-label="${t('ui.topbar.syncAria', {}, 'Sync documentation')}">
|
|
67
|
+
${icon('sync', 16)} ${t('ui.topbar.sync', {}, 'Sync')}
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
<!-- Theme toggle -->
|
|
71
|
+
${theme.renderButton()}
|
|
72
|
+
|
|
73
|
+
<!-- Refresh -->
|
|
74
|
+
<button class="btn btn-ghost btn-sm btn-icon" id="refresh-btn" type="button" aria-label="${t('ui.topbar.refresh', {}, 'Refresh state')}">
|
|
75
|
+
${icon('refresh', 16)}
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
_bindEvents();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _renderRepoBadge(runtime) {
|
|
86
|
+
const isClean = runtime.clean;
|
|
87
|
+
const label = isClean
|
|
88
|
+
? t('ui.topbar.repoClean', {}, 'Clean repo')
|
|
89
|
+
: t('ui.topbar.repoDirty', {
|
|
90
|
+
staged: runtime.staged,
|
|
91
|
+
unstaged: runtime.unstaged,
|
|
92
|
+
untracked: runtime.untracked,
|
|
93
|
+
}, `${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?`);
|
|
94
|
+
return `
|
|
95
|
+
<div class="repo-badge ${isClean ? 'clean' : 'dirty'}" title="${esc(runtime.branch || '')}">
|
|
96
|
+
<span class="repo-badge-dot" aria-hidden="true"></span>
|
|
97
|
+
<span>${icon('gitBranch', 12)} ${esc(runtime.branch || t('ui.topbar.noBranch', {}, 'no branch'))} · ${label}</span>
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _renderProjectSelector(projects, currentId) {
|
|
103
|
+
const options = projects.map(p =>
|
|
104
|
+
`<option value="${esc(p.id)}" ${p.id === currentId ? 'selected' : ''} ${p.available ? '' : 'disabled'}>
|
|
105
|
+
${esc(p.name)}${p.available ? '' : ` (${t('ui.topbar.unavailable', {}, 'unavailable')})`}
|
|
106
|
+
</option>`
|
|
107
|
+
).join('');
|
|
108
|
+
|
|
109
|
+
return `
|
|
110
|
+
<div class="project-select-wrapper" title="${t('ui.topbar.activeProject', {}, 'Active project')}">
|
|
111
|
+
<select id="project-select" aria-label="${t('ui.topbar.activeProject', {}, 'Active project')}">
|
|
112
|
+
${options}
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _renderLocaleSelector(currentLocale) {
|
|
119
|
+
return `
|
|
120
|
+
<div class="project-select-wrapper locale-select-wrapper" title="${t('ui.topbar.language', {}, 'Language')}">
|
|
121
|
+
<select id="locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
|
|
122
|
+
<option value="es" ${currentLocale === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
|
|
123
|
+
<option value="en" ${currentLocale === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
|
|
124
|
+
</select>
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _applyLocaleState(payload) {
|
|
130
|
+
if (!payload) return;
|
|
131
|
+
state.update('payload', payload);
|
|
132
|
+
if (payload.i18n) {
|
|
133
|
+
state.update({
|
|
134
|
+
phases: payload.i18n.phases || [],
|
|
135
|
+
statusLabels: payload.i18n.statusLabels || {},
|
|
136
|
+
locale: payload.i18n.locale || 'es',
|
|
137
|
+
messages: payload.i18n.messages || {},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _bindEvents() {
|
|
143
|
+
// Hamburger → sidebar open
|
|
144
|
+
document.getElementById('sidebar-toggle')?.addEventListener('click', () => {
|
|
145
|
+
const sidebar = document.getElementById('sidebar');
|
|
146
|
+
const isOpen = sidebar?.classList.toggle('is-open');
|
|
147
|
+
document.getElementById('sidebar-toggle')?.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Theme toggle
|
|
151
|
+
theme.bindButton();
|
|
152
|
+
|
|
153
|
+
// Cerrar sidebar al hacer clic fuera (mobile)
|
|
154
|
+
document.addEventListener('click', e => {
|
|
155
|
+
const sidebar = document.getElementById('sidebar');
|
|
156
|
+
const toggle = document.getElementById('sidebar-toggle');
|
|
157
|
+
if (sidebar?.classList.contains('is-open') &&
|
|
158
|
+
!sidebar.contains(e.target) && !toggle?.contains(e.target)) {
|
|
159
|
+
sidebar.classList.remove('is-open');
|
|
160
|
+
toggle?.setAttribute('aria-expanded', 'false');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Project selector
|
|
165
|
+
document.getElementById('project-select')?.addEventListener('change', async e => {
|
|
166
|
+
const id = e.target.value;
|
|
167
|
+
state.update('currentProjectId', id);
|
|
168
|
+
localStorage.setItem('ops-dashboard-project', id);
|
|
169
|
+
state.update('selectedTaskId', null);
|
|
170
|
+
// Trigger refresh global
|
|
171
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
document.getElementById('locale-select')?.addEventListener('change', async e => {
|
|
175
|
+
const select = e.target;
|
|
176
|
+
const previousLocale = state.get('locale') || 'es';
|
|
177
|
+
const nextLocale = select.value;
|
|
178
|
+
|
|
179
|
+
if (nextLocale === previousLocale) return;
|
|
180
|
+
|
|
181
|
+
select.disabled = true;
|
|
182
|
+
try {
|
|
183
|
+
const result = await api.updateProjectLocale(nextLocale);
|
|
184
|
+
_applyLocaleState(result.state);
|
|
185
|
+
flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
|
|
186
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
select.value = previousLocale;
|
|
189
|
+
flash(err.message, 'error');
|
|
190
|
+
} finally {
|
|
191
|
+
select.disabled = false;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Sync button
|
|
196
|
+
document.getElementById('sync-btn')?.addEventListener('click', async () => {
|
|
197
|
+
const btn = document.getElementById('sync-btn');
|
|
198
|
+
if (btn) { btn.disabled = true; btn.innerHTML = `${icon('sync', 16)} ${t('ui.topbar.syncing', {}, 'Syncing…')}`; }
|
|
199
|
+
try {
|
|
200
|
+
await api.syncDocs();
|
|
201
|
+
flash(t('ui.topbar.synced', {}, 'Documentation synced.'), 'success');
|
|
202
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
flash(err.message, 'error');
|
|
205
|
+
} finally {
|
|
206
|
+
if (btn) { btn.disabled = false; btn.innerHTML = `${icon('sync', 16)} ${t('ui.topbar.sync', {}, 'Sync')}`; }
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Refresh button
|
|
211
|
+
document.getElementById('refresh-btn')?.addEventListener('click', () => {
|
|
212
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Búsqueda global (debounced)
|
|
216
|
+
const searchInput = document.getElementById('global-search');
|
|
217
|
+
if (searchInput) {
|
|
218
|
+
const handleSearch = debounce(e => {
|
|
219
|
+
state.update('searchQuery', e.target.value);
|
|
220
|
+
window.dispatchEvent(new CustomEvent('ops:search', { detail: { query: e.target.value } }));
|
|
221
|
+
}, 250);
|
|
222
|
+
searchInput.addEventListener('input', handleSearch);
|
|
223
|
+
|
|
224
|
+
// Atajo de teclado ⌘/Ctrl+F
|
|
225
|
+
document.addEventListener('keydown', e => {
|
|
226
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
searchInput.focus();
|
|
229
|
+
searchInput.select();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Actualiza solo el timer del topbar sin re-renderizar */
|
|
236
|
+
export function updateTimer(display) {
|
|
237
|
+
const el = document.getElementById('topbar-timer-display');
|
|
238
|
+
if (el) el.textContent = display;
|
|
239
|
+
}
|