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.
Files changed (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +660 -575
  3. package/bin/trackops.js +127 -106
  4. package/lib/cli-format.js +118 -0
  5. package/lib/config.js +352 -326
  6. package/lib/control.js +408 -246
  7. package/lib/env.js +234 -222
  8. package/lib/i18n.js +5 -4
  9. package/lib/init.js +390 -282
  10. package/lib/locale.js +41 -41
  11. package/lib/opera-bootstrap.js +1066 -880
  12. package/lib/opera.js +615 -444
  13. package/lib/preferences.js +74 -74
  14. package/lib/registry.js +214 -214
  15. package/lib/release.js +56 -56
  16. package/lib/runtime-state.js +144 -144
  17. package/lib/skills.js +114 -89
  18. package/lib/workspace.js +259 -248
  19. package/locales/en.json +311 -167
  20. package/locales/es.json +314 -170
  21. package/package.json +61 -58
  22. package/scripts/postinstall-locale.js +21 -21
  23. package/scripts/skills-marketplace-smoke.js +124 -124
  24. package/scripts/smoke-tests.js +563 -517
  25. package/scripts/sync-skill-version.js +21 -21
  26. package/scripts/validate-skill.js +103 -103
  27. package/skills/trackops/SKILL.md +126 -122
  28. package/skills/trackops/agents/openai.yaml +7 -7
  29. package/skills/trackops/locales/en/SKILL.md +126 -122
  30. package/skills/trackops/locales/en/references/activation.md +94 -90
  31. package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
  32. package/skills/trackops/locales/en/references/workflow.md +55 -32
  33. package/skills/trackops/references/activation.md +94 -90
  34. package/skills/trackops/references/troubleshooting.md +73 -67
  35. package/skills/trackops/references/workflow.md +55 -32
  36. package/skills/trackops/skill.json +29 -29
  37. package/templates/hooks/post-checkout +2 -2
  38. package/templates/hooks/post-commit +2 -2
  39. package/templates/hooks/post-merge +2 -2
  40. package/templates/opera/agent.md +28 -27
  41. package/templates/opera/architecture/dependency-graph.md +24 -24
  42. package/templates/opera/architecture/runtime-automation.md +24 -24
  43. package/templates/opera/architecture/runtime-operations.md +34 -34
  44. package/templates/opera/en/agent.md +22 -21
  45. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  46. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  47. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  48. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  49. package/templates/opera/en/reviews/integration-audit.md +18 -18
  50. package/templates/opera/en/router.md +24 -19
  51. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  52. package/templates/opera/references/opera-cycle.md +193 -193
  53. package/templates/opera/registry.md +28 -28
  54. package/templates/opera/reviews/delivery-audit.md +18 -18
  55. package/templates/opera/reviews/integration-audit.md +18 -18
  56. package/templates/opera/router.md +54 -49
  57. package/templates/skills/changelog-updater/SKILL.md +69 -69
  58. package/templates/skills/commiter/SKILL.md +99 -99
  59. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  60. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  61. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  62. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  63. package/templates/skills/opera-skill/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  65. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  66. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  67. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  68. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  69. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  70. package/ui/css/base.css +284 -284
  71. package/ui/css/charts.css +425 -425
  72. package/ui/css/components.css +1107 -1107
  73. package/ui/css/onboarding.css +133 -133
  74. package/ui/css/terminal.css +125 -125
  75. package/ui/css/timeline.css +58 -58
  76. package/ui/css/tokens.css +284 -284
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -99
  79. package/ui/js/charts.js +526 -526
  80. package/ui/js/console-logger.js +172 -172
  81. package/ui/js/filters.js +247 -247
  82. package/ui/js/icons.js +129 -129
  83. package/ui/js/keyboard.js +229 -229
  84. package/ui/js/router.js +142 -142
  85. package/ui/js/theme.js +100 -100
  86. package/ui/js/time-tracker.js +248 -248
  87. package/ui/js/views/dashboard.js +870 -870
  88. package/ui/js/views/flash.js +47 -47
  89. package/ui/js/views/projects.js +745 -745
  90. package/ui/js/views/scrum.js +476 -476
  91. package/ui/js/views/settings.js +331 -331
  92. package/ui/js/views/timeline.js +265 -265
@@ -1,331 +1,331 @@
1
- /**
2
- * settings.js — Vista de configuración del proyecto
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, formatDate } from '../utils.js';
10
- import { t } from '../i18n.js';
11
- import * as skillsView from './skills.js';
12
-
13
- function applyLocaleState(payload) {
14
- if (!payload) return;
15
- state.update('payload', payload);
16
- if (payload.i18n) {
17
- state.update({
18
- phases: payload.i18n.phases || [],
19
- statusLabels: payload.i18n.statusLabels || {},
20
- locale: payload.i18n.locale || 'es',
21
- messages: payload.i18n.messages || {},
22
- });
23
- }
24
- }
25
-
26
- function renderWorkspacePanel(payload) {
27
- const project = payload?.project;
28
- if (!project) return '';
29
-
30
- return `
31
- <div class="glass-card panel">
32
- <div class="panel-header">
33
- <p class="panel-title">${t('ui.settings.workspaceTitle', {}, 'Workspace')}</p>
34
- <span class="badge badge-muted">${esc(project.layout || 'legacy')}</span>
35
- </div>
36
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
37
- <div class="info-row">
38
- <p class="label-sm">${t('ui.settings.workspaceRoot', {}, 'Workspace root')}</p>
39
- <p class="value">${esc(project.workspaceRoot || project.root || '—')}</p>
40
- </div>
41
- <div class="info-row">
42
- <p class="label-sm">${t('ui.settings.appRoot', {}, 'App root')}</p>
43
- <p class="value">${esc(project.appRoot || project.root || '—')}</p>
44
- </div>
45
- <div class="info-row">
46
- <p class="label-sm">${t('ui.settings.opsRoot', {}, 'Ops root')}</p>
47
- <p class="value">${esc(project.opsRoot || project.root || '—')}</p>
48
- </div>
49
- </div>
50
- </div>
51
- `;
52
- }
53
-
54
- function renderEnvPanel(envState) {
55
- if (!envState) return '';
56
-
57
- const required = Array.isArray(envState.requiredKeys) ? envState.requiredKeys : [];
58
- const present = Array.isArray(envState.presentKeys) ? envState.presentKeys : [];
59
- const missing = Array.isArray(envState.missingKeys) ? envState.missingKeys : [];
60
- const files = envState.files || {};
61
-
62
- return `
63
- <div class="glass-card panel">
64
- <div class="panel-header">
65
- <p class="panel-title">${t('ui.settings.envTitle', {}, 'Environment')}</p>
66
- <span class="badge badge-${missing.length ? 'warning' : 'success'}">${missing.length ? t('ui.settings.envMissingBadge', { count: missing.length }, `${missing.length} missing`) : t('ui.settings.envHealthyBadge', {}, 'Ready')}</span>
67
- </div>
68
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
69
- <div class="info-row">
70
- <p class="label-sm">${t('ui.settings.envBridgeMode', {}, 'Bridge mode')}</p>
71
- <p class="value">${esc(envState.bridgeMode || 'none')}</p>
72
- </div>
73
- <div class="info-row">
74
- <p class="label-sm">${t('ui.settings.envRootFile', {}, 'Root .env')}</p>
75
- <p class="value">${esc(files.rootEnv || '—')}</p>
76
- </div>
77
- <div class="info-row">
78
- <p class="label-sm">${t('ui.settings.envExampleFile', {}, '.env.example')}</p>
79
- <p class="value">${esc(files.rootExample || '—')}</p>
80
- </div>
81
- <div class="info-row">
82
- <p class="label-sm">${t('ui.settings.envAppBridge', {}, 'App bridge')}</p>
83
- <p class="value">${esc(files.appBridge || '—')}</p>
84
- </div>
85
- <div class="info-row">
86
- <p class="label-sm">${t('ui.settings.envRequiredKeys', {}, 'Required keys')}</p>
87
- <p class="value">${required.length ? esc(required.join(', ')) : '—'}</p>
88
- </div>
89
- <div class="info-row">
90
- <p class="label-sm">${t('ui.settings.envPresentKeys', {}, 'Present keys')}</p>
91
- <p class="value">${present.length ? esc(present.join(', ')) : '—'}</p>
92
- </div>
93
- <div class="info-row">
94
- <p class="label-sm">${t('ui.settings.envMissingKeys', {}, 'Missing keys')}</p>
95
- <p class="value">${missing.length ? esc(missing.join(', ')) : t('ui.settings.envNoMissing', {}, 'None')}</p>
96
- </div>
97
- <div class="info-row">
98
- <p class="label-sm">${t('ui.settings.envLastAudit', {}, 'Last audit')}</p>
99
- <p class="value">${envState.lastAuditAt ? formatDate(envState.lastAuditAt, 'date') : '—'}</p>
100
- </div>
101
- <button class="btn btn-ghost btn-sm" type="button" id="sync-env-btn">
102
- ${icon('sync', 14)} ${t('ui.settings.envSync', {}, 'Sync env')}
103
- </button>
104
- </div>
105
- </div>
106
- `;
107
- }
108
-
109
- export async function render() {
110
- const payload = state.getPayload();
111
- const control = payload?.control;
112
- const runtime = payload?.runtime;
113
- const docsDirty = payload?.docsDirty || [];
114
- const envState = payload?.env;
115
-
116
- return `
117
- <div class="view-enter">
118
- <div class="section-header">
119
- <div class="section-header-left">
120
- <p class="eyebrow">${t('ui.settings.eyebrow', {}, 'Settings')}</p>
121
- <h2>${t('ui.settings.title', {}, 'Project settings')}</h2>
122
- </div>
123
- </div>
124
-
125
- <div class="grid-2" style="align-items:start">
126
-
127
- <!-- Col izquierda -->
128
- <div style="display:flex;flex-direction:column;gap:var(--space-4)">
129
-
130
- <!-- Info del proyecto activo -->
131
- ${control ? `
132
- <div class="glass-card panel">
133
- <div class="panel-header">
134
- <p class="panel-title">Proyecto Activo</p>
135
- <span class="badge badge-success">Activo</span>
136
- </div>
137
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
138
- <div class="info-row">
139
- <p class="label-sm">Nombre</p>
140
- <p class="value">${esc(control.meta?.projectName || '—')}</p>
141
- </div>
142
- <div class="info-row">
143
- <p class="label-sm">Ruta</p>
144
- <p class="value">${esc(payload?.project?.root || '—')}</p>
145
- </div>
146
- <div class="info-row">
147
- <p class="label-sm">Fase activa</p>
148
- <p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
149
- </div>
150
- <div class="info-row">
151
- <p class="label-sm">Foco actual</p>
152
- <p class="value">${esc(control.meta?.currentFocus || '—')}</p>
153
- </div>
154
- <div class="info-row">
155
- <p class="label-sm">Entrega objetivo</p>
156
- <p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
157
- </div>
158
- <div class="info-row">
159
- <p class="label-sm">Última actualización</p>
160
- <p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
161
- </div>
162
- <div class="info-row">
163
- <p class="label-sm">${t('ui.topbar.language', {}, 'Language')}</p>
164
- <div class="project-select-wrapper locale-select-wrapper">
165
- <select id="settings-locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
166
- <option value="es" ${(payload?.i18n?.locale || 'es') === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
167
- <option value="en" ${(payload?.i18n?.locale || 'es') === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
168
- </select>
169
- </div>
170
- </div>
171
- </div>
172
- </div>
173
- ` : `<p class="text-muted">${t('ui.settings.noProject', {}, 'No project loaded.')}</p>`}
174
-
175
- ${renderWorkspacePanel(payload)}
176
-
177
- <!-- Estado del Repo -->
178
- ${runtime ? `
179
- <div class="glass-card panel">
180
- <div class="panel-header">
181
- <p class="panel-title">Repositorio</p>
182
- <span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
183
- </div>
184
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
185
- <div class="info-row">
186
- <p class="label-sm">Rama</p>
187
- <p class="value">${esc(runtime.branch || '—')}</p>
188
- </div>
189
- <div class="info-row">
190
- <p class="label-sm">Estado</p>
191
- <p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
192
- </div>
193
- ${runtime.lastCommit ? `
194
- <div class="info-row">
195
- <p class="label-sm">Último commit</p>
196
- <p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
197
- </div>
198
- <div class="info-row">
199
- <p class="label-sm">Fecha commit</p>
200
- <p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
201
- </div>
202
- ` : ''}
203
- <div class="info-row">
204
- <p class="label-sm">Divergencia</p>
205
- <p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
206
- </div>
207
- </div>
208
- </div>
209
- ` : ''}
210
-
211
- </div>
212
-
213
- <!-- Col derecha -->
214
- <div style="display:flex;flex-direction:column;gap:var(--space-4)">
215
-
216
- <!-- Docs Drift -->
217
- <div class="glass-card panel">
218
- <div class="panel-header">
219
- <p class="panel-title">Documentación</p>
220
- <span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
221
- </div>
222
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
223
- ${docsDirty.length > 0 ? `
224
- <p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
225
- <div class="stack stack-sm">
226
- ${docsDirty.map(d => `
227
- <div class="info-row"><p class="value">${esc(d)}</p></div>
228
- `).join('')}
229
- </div>
230
- ` : `
231
- <p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
232
- `}
233
- <button class="btn btn-ghost btn-sm" type="button" id="sync-docs-btn">
234
- ${icon('sync', 14)} Sincronizar ahora
235
- </button>
236
- </div>
237
- </div>
238
-
239
- ${renderEnvPanel(envState)}
240
-
241
- <!-- Milestones -->
242
- ${control?.milestones?.length ? `
243
- <div class="glass-card panel">
244
- <div class="panel-header">
245
- <p class="panel-title">Milestones</p>
246
- <span class="badge badge-accent">${control.milestones.length}</span>
247
- </div>
248
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
249
- ${control.milestones.map(m => `
250
- <div class="finding-item" style="border-left-color:var(--accent)">
251
- <p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
252
- <p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
253
- <ul style="margin-top:var(--space-2);padding-left:1rem">
254
- ${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
255
- </ul>
256
- </div>
257
- `).join('')}
258
- </div>
259
- </div>
260
- ` : ''}
261
-
262
- <!-- Skills & Extensions -->
263
- <div class="glass-card panel">
264
- <div class="panel-header">
265
- <h3 class="panel-title">${icon('zap', 16)} ${t('ui.settings.skills', {}, 'Skills & Extensions')}</h3>
266
- </div>
267
- <div class="panel-body" id="settings-skills-container">
268
- <p style="color:var(--text-muted);font-size:var(--text-sm)">${t('ui.settings.skillsLoading', {}, 'Loading skills...')}</p>
269
- </div>
270
- </div>
271
-
272
- </div>
273
- </div>
274
- </div>
275
- `;
276
- }
277
-
278
- export async function bindEvents() {
279
- // Sync docs
280
- document.getElementById('sync-docs-btn')?.addEventListener('click', async () => {
281
- try {
282
- await api.syncDocs();
283
- flash('Documentación sincronizada.', 'success');
284
- window.dispatchEvent(new CustomEvent('ops:refresh'));
285
- } catch (err) {
286
- flash(err.message, 'error');
287
- }
288
- });
289
-
290
- document.getElementById('sync-env-btn')?.addEventListener('click', async () => {
291
- try {
292
- await api.syncEnv();
293
- flash(t('ui.settings.envSynced', {}, 'Environment synced.'), 'success');
294
- window.dispatchEvent(new CustomEvent('ops:refresh'));
295
- } catch (err) {
296
- flash(err.message, 'error');
297
- }
298
- });
299
-
300
- document.getElementById('settings-locale-select')?.addEventListener('change', async e => {
301
- const select = e.target;
302
- const previousLocale = state.get('locale') || 'es';
303
- const nextLocale = select.value;
304
-
305
- if (nextLocale === previousLocale) return;
306
-
307
- select.disabled = true;
308
- try {
309
- const result = await api.updateProjectLocale(nextLocale);
310
- applyLocaleState(result.state);
311
- flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
312
- window.dispatchEvent(new CustomEvent('ops:refresh'));
313
- } catch (err) {
314
- select.value = previousLocale;
315
- flash(err.message, 'error');
316
- } finally {
317
- select.disabled = false;
318
- }
319
- });
320
-
321
- // Load skills into the container
322
- try {
323
- const skillsHtml = await skillsView.render();
324
- const container = document.getElementById('settings-skills-container');
325
- if (container) container.innerHTML = skillsHtml;
326
- skillsView.bindEvents();
327
- if (typeof skillsView.loadData === 'function') skillsView.loadData();
328
- } catch (e) { console.warn('Skills load failed:', e); }
329
- }
330
-
331
- export { bindEvents as bind };
1
+ /**
2
+ * settings.js — Vista de configuración del proyecto
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, formatDate } from '../utils.js';
10
+ import { t } from '../i18n.js';
11
+ import * as skillsView from './skills.js';
12
+
13
+ function applyLocaleState(payload) {
14
+ if (!payload) return;
15
+ state.update('payload', payload);
16
+ if (payload.i18n) {
17
+ state.update({
18
+ phases: payload.i18n.phases || [],
19
+ statusLabels: payload.i18n.statusLabels || {},
20
+ locale: payload.i18n.locale || 'es',
21
+ messages: payload.i18n.messages || {},
22
+ });
23
+ }
24
+ }
25
+
26
+ function renderWorkspacePanel(payload) {
27
+ const project = payload?.project;
28
+ if (!project) return '';
29
+
30
+ return `
31
+ <div class="glass-card panel">
32
+ <div class="panel-header">
33
+ <p class="panel-title">${t('ui.settings.workspaceTitle', {}, 'Workspace')}</p>
34
+ <span class="badge badge-muted">${esc(project.layout || 'legacy')}</span>
35
+ </div>
36
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
37
+ <div class="info-row">
38
+ <p class="label-sm">${t('ui.settings.workspaceRoot', {}, 'Workspace root')}</p>
39
+ <p class="value">${esc(project.workspaceRoot || project.root || '—')}</p>
40
+ </div>
41
+ <div class="info-row">
42
+ <p class="label-sm">${t('ui.settings.appRoot', {}, 'App root')}</p>
43
+ <p class="value">${esc(project.appRoot || project.root || '—')}</p>
44
+ </div>
45
+ <div class="info-row">
46
+ <p class="label-sm">${t('ui.settings.opsRoot', {}, 'Ops root')}</p>
47
+ <p class="value">${esc(project.opsRoot || project.root || '—')}</p>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ `;
52
+ }
53
+
54
+ function renderEnvPanel(envState) {
55
+ if (!envState) return '';
56
+
57
+ const required = Array.isArray(envState.requiredKeys) ? envState.requiredKeys : [];
58
+ const present = Array.isArray(envState.presentKeys) ? envState.presentKeys : [];
59
+ const missing = Array.isArray(envState.missingKeys) ? envState.missingKeys : [];
60
+ const files = envState.files || {};
61
+
62
+ return `
63
+ <div class="glass-card panel">
64
+ <div class="panel-header">
65
+ <p class="panel-title">${t('ui.settings.envTitle', {}, 'Environment')}</p>
66
+ <span class="badge badge-${missing.length ? 'warning' : 'success'}">${missing.length ? t('ui.settings.envMissingBadge', { count: missing.length }, `${missing.length} missing`) : t('ui.settings.envHealthyBadge', {}, 'Ready')}</span>
67
+ </div>
68
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
69
+ <div class="info-row">
70
+ <p class="label-sm">${t('ui.settings.envBridgeMode', {}, 'Bridge mode')}</p>
71
+ <p class="value">${esc(envState.bridgeMode || 'none')}</p>
72
+ </div>
73
+ <div class="info-row">
74
+ <p class="label-sm">${t('ui.settings.envRootFile', {}, 'Root .env')}</p>
75
+ <p class="value">${esc(files.rootEnv || '—')}</p>
76
+ </div>
77
+ <div class="info-row">
78
+ <p class="label-sm">${t('ui.settings.envExampleFile', {}, '.env.example')}</p>
79
+ <p class="value">${esc(files.rootExample || '—')}</p>
80
+ </div>
81
+ <div class="info-row">
82
+ <p class="label-sm">${t('ui.settings.envAppBridge', {}, 'App bridge')}</p>
83
+ <p class="value">${esc(files.appBridge || '—')}</p>
84
+ </div>
85
+ <div class="info-row">
86
+ <p class="label-sm">${t('ui.settings.envRequiredKeys', {}, 'Required keys')}</p>
87
+ <p class="value">${required.length ? esc(required.join(', ')) : '—'}</p>
88
+ </div>
89
+ <div class="info-row">
90
+ <p class="label-sm">${t('ui.settings.envPresentKeys', {}, 'Present keys')}</p>
91
+ <p class="value">${present.length ? esc(present.join(', ')) : '—'}</p>
92
+ </div>
93
+ <div class="info-row">
94
+ <p class="label-sm">${t('ui.settings.envMissingKeys', {}, 'Missing keys')}</p>
95
+ <p class="value">${missing.length ? esc(missing.join(', ')) : t('ui.settings.envNoMissing', {}, 'None')}</p>
96
+ </div>
97
+ <div class="info-row">
98
+ <p class="label-sm">${t('ui.settings.envLastAudit', {}, 'Last audit')}</p>
99
+ <p class="value">${envState.lastAuditAt ? formatDate(envState.lastAuditAt, 'date') : '—'}</p>
100
+ </div>
101
+ <button class="btn btn-ghost btn-sm" type="button" id="sync-env-btn">
102
+ ${icon('sync', 14)} ${t('ui.settings.envSync', {}, 'Sync env')}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ `;
107
+ }
108
+
109
+ export async function render() {
110
+ const payload = state.getPayload();
111
+ const control = payload?.control;
112
+ const runtime = payload?.runtime;
113
+ const docsDirty = payload?.docsDirty || [];
114
+ const envState = payload?.env;
115
+
116
+ return `
117
+ <div class="view-enter">
118
+ <div class="section-header">
119
+ <div class="section-header-left">
120
+ <p class="eyebrow">${t('ui.settings.eyebrow', {}, 'Settings')}</p>
121
+ <h2>${t('ui.settings.title', {}, 'Project settings')}</h2>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="grid-2" style="align-items:start">
126
+
127
+ <!-- Col izquierda -->
128
+ <div style="display:flex;flex-direction:column;gap:var(--space-4)">
129
+
130
+ <!-- Info del proyecto activo -->
131
+ ${control ? `
132
+ <div class="glass-card panel">
133
+ <div class="panel-header">
134
+ <p class="panel-title">Proyecto Activo</p>
135
+ <span class="badge badge-success">Activo</span>
136
+ </div>
137
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
138
+ <div class="info-row">
139
+ <p class="label-sm">Nombre</p>
140
+ <p class="value">${esc(control.meta?.projectName || '—')}</p>
141
+ </div>
142
+ <div class="info-row">
143
+ <p class="label-sm">Ruta</p>
144
+ <p class="value">${esc(payload?.project?.root || '—')}</p>
145
+ </div>
146
+ <div class="info-row">
147
+ <p class="label-sm">Fase activa</p>
148
+ <p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
149
+ </div>
150
+ <div class="info-row">
151
+ <p class="label-sm">Foco actual</p>
152
+ <p class="value">${esc(control.meta?.currentFocus || '—')}</p>
153
+ </div>
154
+ <div class="info-row">
155
+ <p class="label-sm">Entrega objetivo</p>
156
+ <p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
157
+ </div>
158
+ <div class="info-row">
159
+ <p class="label-sm">Última actualización</p>
160
+ <p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
161
+ </div>
162
+ <div class="info-row">
163
+ <p class="label-sm">${t('ui.topbar.language', {}, 'Language')}</p>
164
+ <div class="project-select-wrapper locale-select-wrapper">
165
+ <select id="settings-locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
166
+ <option value="es" ${(payload?.i18n?.locale || 'es') === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
167
+ <option value="en" ${(payload?.i18n?.locale || 'es') === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
168
+ </select>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ ` : `<p class="text-muted">${t('ui.settings.noProject', {}, 'No project loaded.')}</p>`}
174
+
175
+ ${renderWorkspacePanel(payload)}
176
+
177
+ <!-- Estado del Repo -->
178
+ ${runtime ? `
179
+ <div class="glass-card panel">
180
+ <div class="panel-header">
181
+ <p class="panel-title">Repositorio</p>
182
+ <span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
183
+ </div>
184
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
185
+ <div class="info-row">
186
+ <p class="label-sm">Rama</p>
187
+ <p class="value">${esc(runtime.branch || '—')}</p>
188
+ </div>
189
+ <div class="info-row">
190
+ <p class="label-sm">Estado</p>
191
+ <p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
192
+ </div>
193
+ ${runtime.lastCommit ? `
194
+ <div class="info-row">
195
+ <p class="label-sm">Último commit</p>
196
+ <p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
197
+ </div>
198
+ <div class="info-row">
199
+ <p class="label-sm">Fecha commit</p>
200
+ <p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
201
+ </div>
202
+ ` : ''}
203
+ <div class="info-row">
204
+ <p class="label-sm">Divergencia</p>
205
+ <p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ ` : ''}
210
+
211
+ </div>
212
+
213
+ <!-- Col derecha -->
214
+ <div style="display:flex;flex-direction:column;gap:var(--space-4)">
215
+
216
+ <!-- Docs Drift -->
217
+ <div class="glass-card panel">
218
+ <div class="panel-header">
219
+ <p class="panel-title">Documentación</p>
220
+ <span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
221
+ </div>
222
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
223
+ ${docsDirty.length > 0 ? `
224
+ <p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
225
+ <div class="stack stack-sm">
226
+ ${docsDirty.map(d => `
227
+ <div class="info-row"><p class="value">${esc(d)}</p></div>
228
+ `).join('')}
229
+ </div>
230
+ ` : `
231
+ <p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
232
+ `}
233
+ <button class="btn btn-ghost btn-sm" type="button" id="sync-docs-btn">
234
+ ${icon('sync', 14)} Sincronizar ahora
235
+ </button>
236
+ </div>
237
+ </div>
238
+
239
+ ${renderEnvPanel(envState)}
240
+
241
+ <!-- Milestones -->
242
+ ${control?.milestones?.length ? `
243
+ <div class="glass-card panel">
244
+ <div class="panel-header">
245
+ <p class="panel-title">Milestones</p>
246
+ <span class="badge badge-accent">${control.milestones.length}</span>
247
+ </div>
248
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
249
+ ${control.milestones.map(m => `
250
+ <div class="finding-item" style="border-left-color:var(--accent)">
251
+ <p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
252
+ <p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
253
+ <ul style="margin-top:var(--space-2);padding-left:1rem">
254
+ ${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
255
+ </ul>
256
+ </div>
257
+ `).join('')}
258
+ </div>
259
+ </div>
260
+ ` : ''}
261
+
262
+ <!-- Skills & Extensions -->
263
+ <div class="glass-card panel">
264
+ <div class="panel-header">
265
+ <h3 class="panel-title">${icon('zap', 16)} ${t('ui.settings.skills', {}, 'Skills & Extensions')}</h3>
266
+ </div>
267
+ <div class="panel-body" id="settings-skills-container">
268
+ <p style="color:var(--text-muted);font-size:var(--text-sm)">${t('ui.settings.skillsLoading', {}, 'Loading skills...')}</p>
269
+ </div>
270
+ </div>
271
+
272
+ </div>
273
+ </div>
274
+ </div>
275
+ `;
276
+ }
277
+
278
+ export async function bindEvents() {
279
+ // Sync docs
280
+ document.getElementById('sync-docs-btn')?.addEventListener('click', async () => {
281
+ try {
282
+ await api.syncDocs();
283
+ flash('Documentación sincronizada.', 'success');
284
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
285
+ } catch (err) {
286
+ flash(err.message, 'error');
287
+ }
288
+ });
289
+
290
+ document.getElementById('sync-env-btn')?.addEventListener('click', async () => {
291
+ try {
292
+ await api.syncEnv();
293
+ flash(t('ui.settings.envSynced', {}, 'Environment synced.'), 'success');
294
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
295
+ } catch (err) {
296
+ flash(err.message, 'error');
297
+ }
298
+ });
299
+
300
+ document.getElementById('settings-locale-select')?.addEventListener('change', async e => {
301
+ const select = e.target;
302
+ const previousLocale = state.get('locale') || 'es';
303
+ const nextLocale = select.value;
304
+
305
+ if (nextLocale === previousLocale) return;
306
+
307
+ select.disabled = true;
308
+ try {
309
+ const result = await api.updateProjectLocale(nextLocale);
310
+ applyLocaleState(result.state);
311
+ flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
312
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
313
+ } catch (err) {
314
+ select.value = previousLocale;
315
+ flash(err.message, 'error');
316
+ } finally {
317
+ select.disabled = false;
318
+ }
319
+ });
320
+
321
+ // Load skills into the container
322
+ try {
323
+ const skillsHtml = await skillsView.render();
324
+ const container = document.getElementById('settings-skills-container');
325
+ if (container) container.innerHTML = skillsHtml;
326
+ skillsView.bindEvents();
327
+ if (typeof skillsView.loadData === 'function') skillsView.loadData();
328
+ } catch (e) { console.warn('Skills load failed:', e); }
329
+ }
330
+
331
+ export { bindEvents as bind };