trackops 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +660 -575
- package/bin/trackops.js +127 -106
- package/lib/cli-format.js +118 -0
- package/lib/config.js +352 -326
- package/lib/control.js +408 -246
- package/lib/env.js +234 -222
- package/lib/i18n.js +5 -4
- package/lib/init.js +390 -282
- package/lib/locale.js +41 -41
- package/lib/opera-bootstrap.js +1066 -880
- package/lib/opera.js +615 -444
- package/lib/preferences.js +74 -74
- package/lib/registry.js +214 -214
- package/lib/release.js +56 -56
- package/lib/runtime-state.js +144 -144
- package/lib/skills.js +114 -89
- package/lib/workspace.js +259 -248
- package/locales/en.json +311 -167
- package/locales/es.json +314 -170
- package/package.json +61 -58
- package/scripts/postinstall-locale.js +21 -21
- package/scripts/skills-marketplace-smoke.js +124 -124
- package/scripts/smoke-tests.js +563 -517
- package/scripts/sync-skill-version.js +21 -21
- package/scripts/validate-skill.js +103 -103
- package/skills/trackops/SKILL.md +126 -122
- package/skills/trackops/agents/openai.yaml +7 -7
- package/skills/trackops/locales/en/SKILL.md +126 -122
- package/skills/trackops/locales/en/references/activation.md +94 -90
- package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
- package/skills/trackops/locales/en/references/workflow.md +55 -32
- package/skills/trackops/references/activation.md +94 -90
- package/skills/trackops/references/troubleshooting.md +73 -67
- package/skills/trackops/references/workflow.md +55 -32
- package/skills/trackops/skill.json +29 -29
- package/templates/hooks/post-checkout +2 -2
- package/templates/hooks/post-commit +2 -2
- package/templates/hooks/post-merge +2 -2
- package/templates/opera/agent.md +28 -27
- package/templates/opera/architecture/dependency-graph.md +24 -24
- package/templates/opera/architecture/runtime-automation.md +24 -24
- package/templates/opera/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/agent.md +22 -21
- package/templates/opera/en/architecture/dependency-graph.md +24 -24
- package/templates/opera/en/architecture/runtime-automation.md +24 -24
- package/templates/opera/en/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/reviews/delivery-audit.md +18 -18
- package/templates/opera/en/reviews/integration-audit.md +18 -18
- package/templates/opera/en/router.md +24 -19
- package/templates/opera/references/autonomy-and-recovery.md +117 -117
- package/templates/opera/references/opera-cycle.md +193 -193
- package/templates/opera/registry.md +28 -28
- package/templates/opera/reviews/delivery-audit.md +18 -18
- package/templates/opera/reviews/integration-audit.md +18 -18
- package/templates/opera/router.md +54 -49
- package/templates/skills/changelog-updater/SKILL.md +69 -69
- package/templates/skills/commiter/SKILL.md +99 -99
- package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
- package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
- package/templates/skills/opera-policy-guard/SKILL.md +26 -26
- package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
- package/templates/skills/opera-skill/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
- package/templates/skills/opera-skill/references/phase-dod.md +138 -0
- package/templates/skills/project-starter-skill/SKILL.md +150 -131
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
- package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
- package/ui/css/base.css +284 -284
- package/ui/css/charts.css +425 -425
- package/ui/css/components.css +1107 -1107
- package/ui/css/onboarding.css +133 -133
- package/ui/css/terminal.css +125 -125
- package/ui/css/timeline.css +58 -58
- package/ui/css/tokens.css +284 -284
- package/ui/favicon.svg +5 -5
- package/ui/index.html +99 -99
- package/ui/js/charts.js +526 -526
- package/ui/js/console-logger.js +172 -172
- package/ui/js/filters.js +247 -247
- package/ui/js/icons.js +129 -129
- package/ui/js/keyboard.js +229 -229
- package/ui/js/router.js +142 -142
- package/ui/js/theme.js +100 -100
- package/ui/js/time-tracker.js +248 -248
- package/ui/js/views/dashboard.js +870 -870
- package/ui/js/views/flash.js +47 -47
- package/ui/js/views/projects.js +745 -745
- package/ui/js/views/scrum.js +476 -476
- package/ui/js/views/settings.js +331 -331
- package/ui/js/views/timeline.js +265 -265
package/ui/js/time-tracker.js
CHANGED
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* time-tracker.js — Cronómetro de tiempo por tarea
|
|
3
|
-
* Integración con la API de time tracking del backend.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as state from './state.js';
|
|
7
|
-
import * as api from './api.js';
|
|
8
|
-
import { formatDuration } from './utils.js';
|
|
9
|
-
import { flash } from './views/flash.js';
|
|
10
|
-
|
|
11
|
-
let _interval = null;
|
|
12
|
-
let _startMs = null;
|
|
13
|
-
|
|
14
|
-
// ─────────────────────────────── PÚBLICO ────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Inicia el timer para una tarea
|
|
18
|
-
* @param {string} taskId
|
|
19
|
-
* @param {string} taskTitle
|
|
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
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const result = await api.startTimeEntry(taskId, taskTitle);
|
|
29
|
-
const entry = {
|
|
30
|
-
id: result.entry?.id || `local-${Date.now()}`,
|
|
31
|
-
taskId,
|
|
32
|
-
taskTitle,
|
|
33
|
-
startedAt: result.entry?.startedAt || new Date().toISOString(),
|
|
34
|
-
};
|
|
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(),
|
|
47
|
-
};
|
|
48
|
-
state.update('activeEntry', entry);
|
|
49
|
-
_startMs = Date.now();
|
|
50
|
-
_startInterval();
|
|
51
|
-
_updateTopbarTimer();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Pausa el timer (detiene el interval pero conserva el entry)
|
|
57
|
-
*/
|
|
58
|
-
export function pause() {
|
|
59
|
-
_stopInterval();
|
|
60
|
-
_updateTopbarTimer();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Reanuda el timer
|
|
65
|
-
*/
|
|
66
|
-
export function resume() {
|
|
67
|
-
if (!state.get('activeEntry')) return;
|
|
68
|
-
_startInterval();
|
|
69
|
-
_updateTopbarTimer();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Detiene el timer y persiste en el backend
|
|
74
|
-
*/
|
|
75
|
-
export async function stop() {
|
|
76
|
-
const entry = state.get('activeEntry');
|
|
77
|
-
if (!entry) return;
|
|
78
|
-
|
|
79
|
-
_stopInterval();
|
|
80
|
-
const elapsed = _startMs ? Date.now() - _startMs : 0;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const result = await api.stopTimeEntry(entry.id);
|
|
84
|
-
const timeEntries = state.get('timeEntries');
|
|
85
|
-
timeEntries.unshift({
|
|
86
|
-
...entry,
|
|
87
|
-
stoppedAt: new Date().toISOString(),
|
|
88
|
-
durationMs: result.entry?.durationMs || elapsed,
|
|
89
|
-
});
|
|
90
|
-
state.update('timeEntries', timeEntries.slice(0, 50)); // Máximo 50 entries
|
|
91
|
-
} catch {
|
|
92
|
-
// Guardar localmente si el backend no está disponible
|
|
93
|
-
const timeEntries = state.get('timeEntries');
|
|
94
|
-
timeEntries.unshift({
|
|
95
|
-
...entry,
|
|
96
|
-
stoppedAt: new Date().toISOString(),
|
|
97
|
-
durationMs: elapsed,
|
|
98
|
-
});
|
|
99
|
-
state.update('timeEntries', timeEntries.slice(0, 50));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
state.update('activeEntry', null);
|
|
103
|
-
_startMs = null;
|
|
104
|
-
_updateTopbarTimer();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Obtiene el tiempo transcurrido actual en ms
|
|
109
|
-
*/
|
|
110
|
-
export function getElapsed() {
|
|
111
|
-
if (!state.get('activeEntry') || !_startMs) return 0;
|
|
112
|
-
return Date.now() - _startMs;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Obtiene el total de tiempo registrado para una tarea (en ms)
|
|
117
|
-
* @param {string} taskId
|
|
118
|
-
*/
|
|
119
|
-
export function getTotalForTask(taskId) {
|
|
120
|
-
return state.get('timeEntries')
|
|
121
|
-
.filter(e => e.taskId === taskId)
|
|
122
|
-
.reduce((acc, e) => acc + (e.durationMs || 0), 0);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Carga los time entries del backend
|
|
127
|
-
*/
|
|
128
|
-
export async function loadEntries() {
|
|
129
|
-
try {
|
|
130
|
-
const result = await api.getTimeEntries();
|
|
131
|
-
state.update('timeEntries', result.entries || []);
|
|
132
|
-
} catch {
|
|
133
|
-
// Si el endpoint no existe aún, usar array vacío
|
|
134
|
-
state.update('timeEntries', []);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─────────────────────────────── PRIVADO ────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
function _startInterval() {
|
|
141
|
-
_stopInterval();
|
|
142
|
-
_interval = setInterval(() => {
|
|
143
|
-
_updateTimerDisplays();
|
|
144
|
-
}, 1000);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function _stopInterval() {
|
|
148
|
-
if (_interval) {
|
|
149
|
-
clearInterval(_interval);
|
|
150
|
-
_interval = null;
|
|
151
|
-
}
|
|
152
|
-
state.update('timerInterval', null);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function _updateTimerDisplays() {
|
|
156
|
-
const elapsed = getElapsed();
|
|
157
|
-
const formatted = formatDuration(elapsed);
|
|
158
|
-
|
|
159
|
-
// Topbar timer
|
|
160
|
-
const topbarDisplay = document.getElementById('topbar-timer-display');
|
|
161
|
-
if (topbarDisplay) topbarDisplay.textContent = formatted;
|
|
162
|
-
|
|
163
|
-
// Widget grande (si está visible en Overview)
|
|
164
|
-
const bigDisplay = document.querySelector('.timer-display');
|
|
165
|
-
if (bigDisplay) bigDisplay.textContent = formatted;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function _updateTopbarTimer() {
|
|
169
|
-
const entry = state.get('activeEntry');
|
|
170
|
-
const topbarTimer = document.getElementById('topbar-timer');
|
|
171
|
-
if (!topbarTimer) return;
|
|
172
|
-
|
|
173
|
-
if (entry) {
|
|
174
|
-
topbarTimer.classList.add('is-running');
|
|
175
|
-
const dot = topbarTimer.querySelector('.topbar-timer-dot');
|
|
176
|
-
const display = document.getElementById('topbar-timer-display');
|
|
177
|
-
if (display) display.textContent = formatDuration(getElapsed());
|
|
178
|
-
} else {
|
|
179
|
-
topbarTimer.classList.remove('is-running');
|
|
180
|
-
const display = document.getElementById('topbar-timer-display');
|
|
181
|
-
if (display) display.textContent = '00:00:00';
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Renderiza el widget grande del timer (para overview.js)
|
|
187
|
-
* @param {string|null} taskId - tarea preseleccionada
|
|
188
|
-
* @returns {string} HTML
|
|
189
|
-
*/
|
|
190
|
-
export function renderWidget(taskId = null) {
|
|
191
|
-
const entry = state.get('activeEntry');
|
|
192
|
-
const isRunning = !!entry && (!taskId || entry.taskId === taskId);
|
|
193
|
-
const elapsed = isRunning ? getElapsed() : 0;
|
|
194
|
-
const totalMs = taskId ? getTotalForTask(taskId) : 0;
|
|
195
|
-
|
|
196
|
-
const taskTitle = entry?.taskTitle || 'Sin tarea seleccionada';
|
|
197
|
-
|
|
198
|
-
return `
|
|
199
|
-
<div class="time-tracker-card" id="time-tracker-widget">
|
|
200
|
-
<div class="section-header" style="margin-bottom:var(--space-2)">
|
|
201
|
-
<div class="section-header-left">
|
|
202
|
-
<p class="eyebrow">Time Tracker</p>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<p class="timer-task-name" id="timer-task-name">${isRunning ? taskTitle : (taskId ? 'Clic en Iniciar para comenzar' : 'Selecciona una tarea en el board')}</p>
|
|
207
|
-
|
|
208
|
-
<div class="timer-display ${isRunning ? 'is-running' : ''}" id="timer-display">
|
|
209
|
-
${formatDuration(elapsed)}
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<div class="timer-controls">
|
|
213
|
-
${isRunning ? `
|
|
214
|
-
<button class="timer-btn timer-btn-stop" id="timer-stop" type="button" aria-label="Detener timer" title="Detener">
|
|
215
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
|
216
|
-
</button>
|
|
217
|
-
<button class="timer-btn timer-btn-pause timer-btn-play" id="timer-pause" type="button" aria-label="Pausar timer" title="Pausar">
|
|
218
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
|
219
|
-
</button>
|
|
220
|
-
` : `
|
|
221
|
-
<button class="timer-btn timer-btn-play" id="timer-play" type="button" aria-label="Iniciar timer" title="Iniciar" ${!taskId ? 'disabled' : ''}>
|
|
222
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
223
|
-
</button>
|
|
224
|
-
`}
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
${totalMs > 0 ? `<p class="timer-total">Total registrado: <strong>${formatDuration(totalMs)}</strong></p>` : ''}
|
|
228
|
-
</div>
|
|
229
|
-
`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Vincula los eventos del widget del timer al DOM
|
|
234
|
-
* @param {string|null} taskId
|
|
235
|
-
* @param {string|null} taskTitle
|
|
236
|
-
*/
|
|
237
|
-
export function bindWidget(taskId, taskTitle) {
|
|
238
|
-
const playBtn = document.getElementById('timer-play');
|
|
239
|
-
const pauseBtn = document.getElementById('timer-pause');
|
|
240
|
-
const stopBtn = document.getElementById('timer-stop');
|
|
241
|
-
|
|
242
|
-
playBtn?.addEventListener('click', () => start(taskId, taskTitle));
|
|
243
|
-
pauseBtn?.addEventListener('click', () => {
|
|
244
|
-
if (_interval) pause();
|
|
245
|
-
else resume();
|
|
246
|
-
});
|
|
247
|
-
stopBtn?.addEventListener('click', stop);
|
|
248
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* time-tracker.js — Cronómetro de tiempo por tarea
|
|
3
|
+
* Integración con la API de time tracking del backend.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as state from './state.js';
|
|
7
|
+
import * as api from './api.js';
|
|
8
|
+
import { formatDuration } from './utils.js';
|
|
9
|
+
import { flash } from './views/flash.js';
|
|
10
|
+
|
|
11
|
+
let _interval = null;
|
|
12
|
+
let _startMs = null;
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────── PÚBLICO ────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inicia el timer para una tarea
|
|
18
|
+
* @param {string} taskId
|
|
19
|
+
* @param {string} taskTitle
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await api.startTimeEntry(taskId, taskTitle);
|
|
29
|
+
const entry = {
|
|
30
|
+
id: result.entry?.id || `local-${Date.now()}`,
|
|
31
|
+
taskId,
|
|
32
|
+
taskTitle,
|
|
33
|
+
startedAt: result.entry?.startedAt || new Date().toISOString(),
|
|
34
|
+
};
|
|
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(),
|
|
47
|
+
};
|
|
48
|
+
state.update('activeEntry', entry);
|
|
49
|
+
_startMs = Date.now();
|
|
50
|
+
_startInterval();
|
|
51
|
+
_updateTopbarTimer();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pausa el timer (detiene el interval pero conserva el entry)
|
|
57
|
+
*/
|
|
58
|
+
export function pause() {
|
|
59
|
+
_stopInterval();
|
|
60
|
+
_updateTopbarTimer();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reanuda el timer
|
|
65
|
+
*/
|
|
66
|
+
export function resume() {
|
|
67
|
+
if (!state.get('activeEntry')) return;
|
|
68
|
+
_startInterval();
|
|
69
|
+
_updateTopbarTimer();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detiene el timer y persiste en el backend
|
|
74
|
+
*/
|
|
75
|
+
export async function stop() {
|
|
76
|
+
const entry = state.get('activeEntry');
|
|
77
|
+
if (!entry) return;
|
|
78
|
+
|
|
79
|
+
_stopInterval();
|
|
80
|
+
const elapsed = _startMs ? Date.now() - _startMs : 0;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await api.stopTimeEntry(entry.id);
|
|
84
|
+
const timeEntries = state.get('timeEntries');
|
|
85
|
+
timeEntries.unshift({
|
|
86
|
+
...entry,
|
|
87
|
+
stoppedAt: new Date().toISOString(),
|
|
88
|
+
durationMs: result.entry?.durationMs || elapsed,
|
|
89
|
+
});
|
|
90
|
+
state.update('timeEntries', timeEntries.slice(0, 50)); // Máximo 50 entries
|
|
91
|
+
} catch {
|
|
92
|
+
// Guardar localmente si el backend no está disponible
|
|
93
|
+
const timeEntries = state.get('timeEntries');
|
|
94
|
+
timeEntries.unshift({
|
|
95
|
+
...entry,
|
|
96
|
+
stoppedAt: new Date().toISOString(),
|
|
97
|
+
durationMs: elapsed,
|
|
98
|
+
});
|
|
99
|
+
state.update('timeEntries', timeEntries.slice(0, 50));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
state.update('activeEntry', null);
|
|
103
|
+
_startMs = null;
|
|
104
|
+
_updateTopbarTimer();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Obtiene el tiempo transcurrido actual en ms
|
|
109
|
+
*/
|
|
110
|
+
export function getElapsed() {
|
|
111
|
+
if (!state.get('activeEntry') || !_startMs) return 0;
|
|
112
|
+
return Date.now() - _startMs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Obtiene el total de tiempo registrado para una tarea (en ms)
|
|
117
|
+
* @param {string} taskId
|
|
118
|
+
*/
|
|
119
|
+
export function getTotalForTask(taskId) {
|
|
120
|
+
return state.get('timeEntries')
|
|
121
|
+
.filter(e => e.taskId === taskId)
|
|
122
|
+
.reduce((acc, e) => acc + (e.durationMs || 0), 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Carga los time entries del backend
|
|
127
|
+
*/
|
|
128
|
+
export async function loadEntries() {
|
|
129
|
+
try {
|
|
130
|
+
const result = await api.getTimeEntries();
|
|
131
|
+
state.update('timeEntries', result.entries || []);
|
|
132
|
+
} catch {
|
|
133
|
+
// Si el endpoint no existe aún, usar array vacío
|
|
134
|
+
state.update('timeEntries', []);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────── PRIVADO ────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function _startInterval() {
|
|
141
|
+
_stopInterval();
|
|
142
|
+
_interval = setInterval(() => {
|
|
143
|
+
_updateTimerDisplays();
|
|
144
|
+
}, 1000);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _stopInterval() {
|
|
148
|
+
if (_interval) {
|
|
149
|
+
clearInterval(_interval);
|
|
150
|
+
_interval = null;
|
|
151
|
+
}
|
|
152
|
+
state.update('timerInterval', null);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _updateTimerDisplays() {
|
|
156
|
+
const elapsed = getElapsed();
|
|
157
|
+
const formatted = formatDuration(elapsed);
|
|
158
|
+
|
|
159
|
+
// Topbar timer
|
|
160
|
+
const topbarDisplay = document.getElementById('topbar-timer-display');
|
|
161
|
+
if (topbarDisplay) topbarDisplay.textContent = formatted;
|
|
162
|
+
|
|
163
|
+
// Widget grande (si está visible en Overview)
|
|
164
|
+
const bigDisplay = document.querySelector('.timer-display');
|
|
165
|
+
if (bigDisplay) bigDisplay.textContent = formatted;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _updateTopbarTimer() {
|
|
169
|
+
const entry = state.get('activeEntry');
|
|
170
|
+
const topbarTimer = document.getElementById('topbar-timer');
|
|
171
|
+
if (!topbarTimer) return;
|
|
172
|
+
|
|
173
|
+
if (entry) {
|
|
174
|
+
topbarTimer.classList.add('is-running');
|
|
175
|
+
const dot = topbarTimer.querySelector('.topbar-timer-dot');
|
|
176
|
+
const display = document.getElementById('topbar-timer-display');
|
|
177
|
+
if (display) display.textContent = formatDuration(getElapsed());
|
|
178
|
+
} else {
|
|
179
|
+
topbarTimer.classList.remove('is-running');
|
|
180
|
+
const display = document.getElementById('topbar-timer-display');
|
|
181
|
+
if (display) display.textContent = '00:00:00';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Renderiza el widget grande del timer (para overview.js)
|
|
187
|
+
* @param {string|null} taskId - tarea preseleccionada
|
|
188
|
+
* @returns {string} HTML
|
|
189
|
+
*/
|
|
190
|
+
export function renderWidget(taskId = null) {
|
|
191
|
+
const entry = state.get('activeEntry');
|
|
192
|
+
const isRunning = !!entry && (!taskId || entry.taskId === taskId);
|
|
193
|
+
const elapsed = isRunning ? getElapsed() : 0;
|
|
194
|
+
const totalMs = taskId ? getTotalForTask(taskId) : 0;
|
|
195
|
+
|
|
196
|
+
const taskTitle = entry?.taskTitle || 'Sin tarea seleccionada';
|
|
197
|
+
|
|
198
|
+
return `
|
|
199
|
+
<div class="time-tracker-card" id="time-tracker-widget">
|
|
200
|
+
<div class="section-header" style="margin-bottom:var(--space-2)">
|
|
201
|
+
<div class="section-header-left">
|
|
202
|
+
<p class="eyebrow">Time Tracker</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<p class="timer-task-name" id="timer-task-name">${isRunning ? taskTitle : (taskId ? 'Clic en Iniciar para comenzar' : 'Selecciona una tarea en el board')}</p>
|
|
207
|
+
|
|
208
|
+
<div class="timer-display ${isRunning ? 'is-running' : ''}" id="timer-display">
|
|
209
|
+
${formatDuration(elapsed)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="timer-controls">
|
|
213
|
+
${isRunning ? `
|
|
214
|
+
<button class="timer-btn timer-btn-stop" id="timer-stop" type="button" aria-label="Detener timer" title="Detener">
|
|
215
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
|
216
|
+
</button>
|
|
217
|
+
<button class="timer-btn timer-btn-pause timer-btn-play" id="timer-pause" type="button" aria-label="Pausar timer" title="Pausar">
|
|
218
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
|
219
|
+
</button>
|
|
220
|
+
` : `
|
|
221
|
+
<button class="timer-btn timer-btn-play" id="timer-play" type="button" aria-label="Iniciar timer" title="Iniciar" ${!taskId ? 'disabled' : ''}>
|
|
222
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
223
|
+
</button>
|
|
224
|
+
`}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
${totalMs > 0 ? `<p class="timer-total">Total registrado: <strong>${formatDuration(totalMs)}</strong></p>` : ''}
|
|
228
|
+
</div>
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Vincula los eventos del widget del timer al DOM
|
|
234
|
+
* @param {string|null} taskId
|
|
235
|
+
* @param {string|null} taskTitle
|
|
236
|
+
*/
|
|
237
|
+
export function bindWidget(taskId, taskTitle) {
|
|
238
|
+
const playBtn = document.getElementById('timer-play');
|
|
239
|
+
const pauseBtn = document.getElementById('timer-pause');
|
|
240
|
+
const stopBtn = document.getElementById('timer-stop');
|
|
241
|
+
|
|
242
|
+
playBtn?.addEventListener('click', () => start(taskId, taskTitle));
|
|
243
|
+
pauseBtn?.addEventListener('click', () => {
|
|
244
|
+
if (_interval) pause();
|
|
245
|
+
else resume();
|
|
246
|
+
});
|
|
247
|
+
stopBtn?.addEventListener('click', stop);
|
|
248
|
+
}
|