trackops 1.0.1 → 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 +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- 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 +907 -554
- 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 +7 -9
- 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/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/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/ui/js/views/settings.js
CHANGED
|
@@ -1,12 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* settings.js — Vista de configuración del proyecto
|
|
3
|
-
*/
|
|
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
|
+
|
|
12
|
+
function applyLocaleState(payload) {
|
|
13
|
+
if (!payload) return;
|
|
14
|
+
state.update('payload', payload);
|
|
15
|
+
if (payload.i18n) {
|
|
16
|
+
state.update({
|
|
17
|
+
phases: payload.i18n.phases || [],
|
|
18
|
+
statusLabels: payload.i18n.statusLabels || {},
|
|
19
|
+
locale: payload.i18n.locale || 'es',
|
|
20
|
+
messages: payload.i18n.messages || {},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderWorkspacePanel(payload) {
|
|
26
|
+
const project = payload?.project;
|
|
27
|
+
if (!project) return '';
|
|
4
28
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
return `
|
|
30
|
+
<div class="panel">
|
|
31
|
+
<div class="panel-header">
|
|
32
|
+
<p class="panel-title">${t('ui.settings.workspaceTitle', {}, 'Workspace')}</p>
|
|
33
|
+
<span class="badge badge-muted">${esc(project.layout || 'legacy')}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
36
|
+
<div class="info-row">
|
|
37
|
+
<p class="label-sm">${t('ui.settings.workspaceRoot', {}, 'Workspace root')}</p>
|
|
38
|
+
<p class="value">${esc(project.workspaceRoot || project.root || '—')}</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="info-row">
|
|
41
|
+
<p class="label-sm">${t('ui.settings.appRoot', {}, 'App root')}</p>
|
|
42
|
+
<p class="value">${esc(project.appRoot || project.root || '—')}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="info-row">
|
|
45
|
+
<p class="label-sm">${t('ui.settings.opsRoot', {}, 'Ops root')}</p>
|
|
46
|
+
<p class="value">${esc(project.opsRoot || project.root || '—')}</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderEnvPanel(envState) {
|
|
54
|
+
if (!envState) return '';
|
|
55
|
+
|
|
56
|
+
const required = Array.isArray(envState.requiredKeys) ? envState.requiredKeys : [];
|
|
57
|
+
const present = Array.isArray(envState.presentKeys) ? envState.presentKeys : [];
|
|
58
|
+
const missing = Array.isArray(envState.missingKeys) ? envState.missingKeys : [];
|
|
59
|
+
const files = envState.files || {};
|
|
60
|
+
|
|
61
|
+
return `
|
|
62
|
+
<div class="panel">
|
|
63
|
+
<div class="panel-header">
|
|
64
|
+
<p class="panel-title">${t('ui.settings.envTitle', {}, 'Environment')}</p>
|
|
65
|
+
<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>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
68
|
+
<div class="info-row">
|
|
69
|
+
<p class="label-sm">${t('ui.settings.envBridgeMode', {}, 'Bridge mode')}</p>
|
|
70
|
+
<p class="value">${esc(envState.bridgeMode || 'none')}</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="info-row">
|
|
73
|
+
<p class="label-sm">${t('ui.settings.envRootFile', {}, 'Root .env')}</p>
|
|
74
|
+
<p class="value">${esc(files.rootEnv || '—')}</p>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="info-row">
|
|
77
|
+
<p class="label-sm">${t('ui.settings.envExampleFile', {}, '.env.example')}</p>
|
|
78
|
+
<p class="value">${esc(files.rootExample || '—')}</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="info-row">
|
|
81
|
+
<p class="label-sm">${t('ui.settings.envAppBridge', {}, 'App bridge')}</p>
|
|
82
|
+
<p class="value">${esc(files.appBridge || '—')}</p>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="info-row">
|
|
85
|
+
<p class="label-sm">${t('ui.settings.envRequiredKeys', {}, 'Required keys')}</p>
|
|
86
|
+
<p class="value">${required.length ? esc(required.join(', ')) : '—'}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="info-row">
|
|
89
|
+
<p class="label-sm">${t('ui.settings.envPresentKeys', {}, 'Present keys')}</p>
|
|
90
|
+
<p class="value">${present.length ? esc(present.join(', ')) : '—'}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="info-row">
|
|
93
|
+
<p class="label-sm">${t('ui.settings.envMissingKeys', {}, 'Missing keys')}</p>
|
|
94
|
+
<p class="value">${missing.length ? esc(missing.join(', ')) : t('ui.settings.envNoMissing', {}, 'None')}</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="info-row">
|
|
97
|
+
<p class="label-sm">${t('ui.settings.envLastAudit', {}, 'Last audit')}</p>
|
|
98
|
+
<p class="value">${envState.lastAuditAt ? formatDate(envState.lastAuditAt, 'date') : '—'}</p>
|
|
99
|
+
</div>
|
|
100
|
+
<button class="btn btn-ghost btn-sm" type="button" id="sync-env-btn">
|
|
101
|
+
${icon('sync', 14)} ${t('ui.settings.envSync', {}, 'Sync env')}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
10
107
|
|
|
11
108
|
export async function render() {
|
|
12
109
|
const payload = state.getPayload();
|
|
@@ -15,229 +112,270 @@ export async function render() {
|
|
|
15
112
|
const control = payload?.control;
|
|
16
113
|
const runtime = payload?.runtime;
|
|
17
114
|
const docsDirty = payload?.docsDirty || [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
115
|
+
const envState = payload?.env;
|
|
116
|
+
|
|
117
|
+
return `
|
|
118
|
+
<div class="view-enter">
|
|
119
|
+
<div class="section-header">
|
|
120
|
+
<div class="section-header-left">
|
|
121
|
+
<p class="eyebrow">${t('ui.settings.eyebrow', {}, 'Settings')}</p>
|
|
122
|
+
<h2>${t('ui.settings.title', {}, 'Project settings')}</h2>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="grid-2" style="align-items:start">
|
|
127
|
+
|
|
128
|
+
<!-- Col izquierda -->
|
|
129
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
130
|
+
|
|
131
|
+
<!-- Info del proyecto activo -->
|
|
34
132
|
${control ? `
|
|
35
133
|
<div class="panel">
|
|
36
134
|
<div class="panel-header">
|
|
37
135
|
<p class="panel-title">Proyecto Activo</p>
|
|
38
|
-
<span class="badge badge-success">Activo</span>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
41
|
-
<div class="info-row">
|
|
42
|
-
<p class="label-sm">Nombre</p>
|
|
43
|
-
<p class="value">${esc(control.meta?.projectName || '—')}</p>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="info-row">
|
|
46
|
-
<p class="label-sm">Ruta</p>
|
|
47
|
-
<p class="value">${esc(payload?.project?.root || '—')}</p>
|
|
48
|
-
</div>
|
|
49
|
-
<div class="info-row">
|
|
50
|
-
<p class="label-sm">Fase activa</p>
|
|
51
|
-
<p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="info-row">
|
|
54
|
-
<p class="label-sm">Foco actual</p>
|
|
55
|
-
<p class="value">${esc(control.meta?.currentFocus || '—')}</p>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="info-row">
|
|
58
|
-
<p class="label-sm">Entrega objetivo</p>
|
|
59
|
-
<p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="info-row">
|
|
62
|
-
<p class="label-sm">Última actualización</p>
|
|
63
|
-
<p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="info-row">
|
|
66
|
-
<p class="label-sm"
|
|
67
|
-
<
|
|
68
|
-
|
|
136
|
+
<span class="badge badge-success">Activo</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
139
|
+
<div class="info-row">
|
|
140
|
+
<p class="label-sm">Nombre</p>
|
|
141
|
+
<p class="value">${esc(control.meta?.projectName || '—')}</p>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="info-row">
|
|
144
|
+
<p class="label-sm">Ruta</p>
|
|
145
|
+
<p class="value">${esc(payload?.project?.root || '—')}</p>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="info-row">
|
|
148
|
+
<p class="label-sm">Fase activa</p>
|
|
149
|
+
<p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="info-row">
|
|
152
|
+
<p class="label-sm">Foco actual</p>
|
|
153
|
+
<p class="value">${esc(control.meta?.currentFocus || '—')}</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="info-row">
|
|
156
|
+
<p class="label-sm">Entrega objetivo</p>
|
|
157
|
+
<p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="info-row">
|
|
160
|
+
<p class="label-sm">Última actualización</p>
|
|
161
|
+
<p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="info-row">
|
|
164
|
+
<p class="label-sm">${t('ui.topbar.language', {}, 'Language')}</p>
|
|
165
|
+
<div class="project-select-wrapper locale-select-wrapper">
|
|
166
|
+
<select id="settings-locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
|
|
167
|
+
<option value="es" ${(payload?.i18n?.locale || 'es') === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
|
|
168
|
+
<option value="en" ${(payload?.i18n?.locale || 'es') === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
|
|
169
|
+
</select>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
69
172
|
</div>
|
|
70
173
|
</div>
|
|
71
|
-
` :
|
|
174
|
+
` : `<p class="text-muted">${t('ui.settings.noProject', {}, 'No project loaded.')}</p>`}
|
|
175
|
+
|
|
176
|
+
${renderWorkspacePanel(payload)}
|
|
72
177
|
|
|
73
178
|
<!-- Estado del Repo -->
|
|
74
179
|
${runtime ? `
|
|
75
180
|
<div class="panel">
|
|
76
|
-
<div class="panel-header">
|
|
77
|
-
<p class="panel-title">Repositorio</p>
|
|
78
|
-
<span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
81
|
-
<div class="info-row">
|
|
82
|
-
<p class="label-sm">Rama</p>
|
|
83
|
-
<p class="value">${esc(runtime.branch || '—')}</p>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="info-row">
|
|
86
|
-
<p class="label-sm">Estado</p>
|
|
87
|
-
<p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
|
|
88
|
-
</div>
|
|
89
|
-
${runtime.lastCommit ? `
|
|
90
|
-
<div class="info-row">
|
|
91
|
-
<p class="label-sm">Último commit</p>
|
|
92
|
-
<p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="info-row">
|
|
95
|
-
<p class="label-sm">Fecha commit</p>
|
|
96
|
-
<p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
|
|
97
|
-
</div>
|
|
98
|
-
` : ''}
|
|
99
|
-
<div class="info-row">
|
|
100
|
-
<p class="label-sm">Divergencia</p>
|
|
101
|
-
<p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
` : ''}
|
|
106
|
-
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<!-- Col derecha -->
|
|
110
|
-
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
111
|
-
|
|
112
|
-
<!-- Portfolio -->
|
|
113
|
-
<div class="panel">
|
|
114
|
-
<div class="panel-header">
|
|
115
|
-
<p class="panel-title">Portfolio de Proyectos</p>
|
|
116
|
-
<span class="badge badge-muted">${projects.length}</span>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
119
|
-
${projects.length === 0
|
|
120
|
-
? '<div class="empty-state">Sin proyectos registrados.</div>'
|
|
121
|
-
: projects.map(p => `
|
|
122
|
-
<div class="project-row ${p.id === currentId ? 'is-active' : ''}">
|
|
123
|
-
<div class="project-row-info">
|
|
124
|
-
<p class="project-name">${esc(p.name)}</p>
|
|
125
|
-
<p class="project-path">${esc(p.root)}</p>
|
|
126
|
-
</div>
|
|
127
|
-
<div class="project-row-actions">
|
|
128
|
-
${p.available
|
|
129
|
-
? `<span class="badge badge-success">Disponible</span>`
|
|
130
|
-
: `<span class="badge badge-warning" title="No se puede cargar el control">No disponible</span>`}
|
|
131
|
-
${p.id === currentId
|
|
132
|
-
? `<span class="badge badge-accent">Activo</span>`
|
|
133
|
-
: p.available ? `<button class="btn btn-ghost btn-sm" type="button" data-switch="${esc(p.id)}">Abrir</button>` : ''
|
|
134
|
-
}
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
`).join('')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
<!-- Registrar nuevo -->
|
|
141
|
-
<div style="margin-top:var(--space-2)">
|
|
142
|
-
<div class="field">
|
|
143
|
-
<label for="new-project-path">Registrar proyecto existente</label>
|
|
144
|
-
<input id="new-project-path" type="text" placeholder="/ruta/al/proyecto" />
|
|
145
|
-
</div>
|
|
146
|
-
<div class="form-actions" style="margin-top:var(--space-2)">
|
|
147
|
-
<button class="btn btn-ghost btn-sm" type="button" id="register-project-btn">
|
|
148
|
-
${icon('plus', 14)} Registrar
|
|
149
|
-
</button>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<!-- Docs Drift -->
|
|
156
|
-
<div class="panel">
|
|
157
|
-
<div class="panel-header">
|
|
158
|
-
<p class="panel-title">Documentación</p>
|
|
159
|
-
<span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
|
|
160
|
-
</div>
|
|
161
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
162
|
-
${docsDirty.length > 0 ? `
|
|
163
|
-
<p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
|
|
164
|
-
<div class="stack stack-sm">
|
|
165
|
-
${docsDirty.map(d => `
|
|
166
|
-
<div class="info-row"><p class="value">${esc(d)}</p></div>
|
|
167
|
-
`).join('')}
|
|
168
|
-
</div>
|
|
169
|
-
` : `
|
|
170
|
-
<p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
|
|
171
|
-
`}
|
|
181
|
+
<div class="panel-header">
|
|
182
|
+
<p class="panel-title">Repositorio</p>
|
|
183
|
+
<span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
186
|
+
<div class="info-row">
|
|
187
|
+
<p class="label-sm">Rama</p>
|
|
188
|
+
<p class="value">${esc(runtime.branch || '—')}</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="info-row">
|
|
191
|
+
<p class="label-sm">Estado</p>
|
|
192
|
+
<p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
|
|
193
|
+
</div>
|
|
194
|
+
${runtime.lastCommit ? `
|
|
195
|
+
<div class="info-row">
|
|
196
|
+
<p class="label-sm">Último commit</p>
|
|
197
|
+
<p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="info-row">
|
|
200
|
+
<p class="label-sm">Fecha commit</p>
|
|
201
|
+
<p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
|
|
202
|
+
</div>
|
|
203
|
+
` : ''}
|
|
204
|
+
<div class="info-row">
|
|
205
|
+
<p class="label-sm">Divergencia</p>
|
|
206
|
+
<p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
` : ''}
|
|
211
|
+
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- Col derecha -->
|
|
215
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
216
|
+
|
|
217
|
+
<!-- Portfolio -->
|
|
218
|
+
<div class="panel">
|
|
219
|
+
<div class="panel-header">
|
|
220
|
+
<p class="panel-title">Portfolio de Proyectos</p>
|
|
221
|
+
<span class="badge badge-muted">${projects.length}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
224
|
+
${projects.length === 0
|
|
225
|
+
? '<div class="empty-state">Sin proyectos registrados.</div>'
|
|
226
|
+
: projects.map(p => `
|
|
227
|
+
<div class="project-row ${p.id === currentId ? 'is-active' : ''}">
|
|
228
|
+
<div class="project-row-info">
|
|
229
|
+
<p class="project-name">${esc(p.name)}</p>
|
|
230
|
+
<p class="project-path">${esc(p.root)}</p>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="project-row-actions">
|
|
233
|
+
${p.available
|
|
234
|
+
? `<span class="badge badge-success">Disponible</span>`
|
|
235
|
+
: `<span class="badge badge-warning" title="No se puede cargar el control">No disponible</span>`}
|
|
236
|
+
${p.id === currentId
|
|
237
|
+
? `<span class="badge badge-accent">Activo</span>`
|
|
238
|
+
: p.available ? `<button class="btn btn-ghost btn-sm" type="button" data-switch="${esc(p.id)}">Abrir</button>` : ''
|
|
239
|
+
}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`).join('')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
<!-- Registrar nuevo -->
|
|
246
|
+
<div style="margin-top:var(--space-2)">
|
|
247
|
+
<div class="field">
|
|
248
|
+
<label for="new-project-path">Registrar proyecto existente</label>
|
|
249
|
+
<input id="new-project-path" type="text" placeholder="/ruta/al/proyecto" />
|
|
250
|
+
</div>
|
|
251
|
+
<div class="form-actions" style="margin-top:var(--space-2)">
|
|
252
|
+
<button class="btn btn-ghost btn-sm" type="button" id="register-project-btn">
|
|
253
|
+
${icon('plus', 14)} Registrar
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- Docs Drift -->
|
|
261
|
+
<div class="panel">
|
|
262
|
+
<div class="panel-header">
|
|
263
|
+
<p class="panel-title">Documentación</p>
|
|
264
|
+
<span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
267
|
+
${docsDirty.length > 0 ? `
|
|
268
|
+
<p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
|
|
269
|
+
<div class="stack stack-sm">
|
|
270
|
+
${docsDirty.map(d => `
|
|
271
|
+
<div class="info-row"><p class="value">${esc(d)}</p></div>
|
|
272
|
+
`).join('')}
|
|
273
|
+
</div>
|
|
274
|
+
` : `
|
|
275
|
+
<p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
|
|
276
|
+
`}
|
|
172
277
|
<button class="btn btn-ghost btn-sm" type="button" id="sync-docs-btn">
|
|
173
278
|
${icon('sync', 14)} Sincronizar ahora
|
|
174
279
|
</button>
|
|
175
280
|
</div>
|
|
176
281
|
</div>
|
|
177
282
|
|
|
283
|
+
${renderEnvPanel(envState)}
|
|
284
|
+
|
|
178
285
|
<!-- Milestones -->
|
|
179
286
|
${control?.milestones?.length ? `
|
|
180
287
|
<div class="panel">
|
|
181
|
-
<div class="panel-header">
|
|
182
|
-
<p class="panel-title">Milestones</p>
|
|
183
|
-
<span class="badge badge-accent">${control.milestones.length}</span>
|
|
184
|
-
</div>
|
|
185
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
186
|
-
${control.milestones.map(m => `
|
|
187
|
-
<div class="finding-item" style="border-left-color:var(--accent)">
|
|
188
|
-
<p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
|
|
189
|
-
<p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
|
|
190
|
-
<ul style="margin-top:var(--space-2);padding-left:1rem">
|
|
191
|
-
${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
|
|
192
|
-
</ul>
|
|
193
|
-
</div>
|
|
194
|
-
`).join('')}
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
` : ''}
|
|
198
|
-
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function bindEvents() {
|
|
206
|
-
// Switch project
|
|
207
|
-
document.querySelectorAll('[data-switch]').forEach(btn => {
|
|
208
|
-
btn.addEventListener('click', () => {
|
|
209
|
-
const id = btn.dataset.switch;
|
|
210
|
-
state.update('currentProjectId', id);
|
|
211
|
-
localStorage.setItem('ops-dashboard-project', id);
|
|
212
|
-
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Registrar proyecto
|
|
217
|
-
document.getElementById('register-project-btn')?.addEventListener('click', async () => {
|
|
218
|
-
const input = document.getElementById('new-project-path');
|
|
219
|
-
const root = input?.value.trim();
|
|
220
|
-
if (!root) { flash('
|
|
288
|
+
<div class="panel-header">
|
|
289
|
+
<p class="panel-title">Milestones</p>
|
|
290
|
+
<span class="badge badge-accent">${control.milestones.length}</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
293
|
+
${control.milestones.map(m => `
|
|
294
|
+
<div class="finding-item" style="border-left-color:var(--accent)">
|
|
295
|
+
<p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
|
|
296
|
+
<p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
|
|
297
|
+
<ul style="margin-top:var(--space-2);padding-left:1rem">
|
|
298
|
+
${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
|
|
299
|
+
</ul>
|
|
300
|
+
</div>
|
|
301
|
+
`).join('')}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
` : ''}
|
|
305
|
+
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function bindEvents() {
|
|
313
|
+
// Switch project
|
|
314
|
+
document.querySelectorAll('[data-switch]').forEach(btn => {
|
|
315
|
+
btn.addEventListener('click', () => {
|
|
316
|
+
const id = btn.dataset.switch;
|
|
317
|
+
state.update('currentProjectId', id);
|
|
318
|
+
localStorage.setItem('ops-dashboard-project', id);
|
|
319
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Registrar proyecto
|
|
324
|
+
document.getElementById('register-project-btn')?.addEventListener('click', async () => {
|
|
325
|
+
const input = document.getElementById('new-project-path');
|
|
326
|
+
const root = input?.value.trim();
|
|
327
|
+
if (!root) { flash(t('ui.settings.enterPath', {}, 'Enter the project path.'), 'warning'); return; }
|
|
328
|
+
try {
|
|
329
|
+
await api.registerProject(root);
|
|
330
|
+
flash('Proyecto registrado.', 'success');
|
|
331
|
+
if (input) input.value = '';
|
|
332
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
flash(err.message, 'error');
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Sync docs
|
|
339
|
+
document.getElementById('sync-docs-btn')?.addEventListener('click', async () => {
|
|
221
340
|
try {
|
|
222
|
-
await api.
|
|
223
|
-
flash('
|
|
224
|
-
if (input) input.value = '';
|
|
341
|
+
await api.syncDocs();
|
|
342
|
+
flash('Documentación sincronizada.', 'success');
|
|
225
343
|
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
226
|
-
} catch (err) {
|
|
344
|
+
} catch (err) {
|
|
227
345
|
flash(err.message, 'error');
|
|
228
346
|
}
|
|
229
347
|
});
|
|
230
348
|
|
|
231
|
-
|
|
232
|
-
document.getElementById('sync-docs-btn')?.addEventListener('click', async () => {
|
|
349
|
+
document.getElementById('sync-env-btn')?.addEventListener('click', async () => {
|
|
233
350
|
try {
|
|
234
|
-
await api.
|
|
235
|
-
flash('
|
|
351
|
+
await api.syncEnv();
|
|
352
|
+
flash(t('ui.settings.envSynced', {}, 'Environment synced.'), 'success');
|
|
236
353
|
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
237
354
|
} catch (err) {
|
|
238
355
|
flash(err.message, 'error');
|
|
239
356
|
}
|
|
240
357
|
});
|
|
241
|
-
}
|
|
242
358
|
|
|
243
|
-
|
|
359
|
+
document.getElementById('settings-locale-select')?.addEventListener('change', async e => {
|
|
360
|
+
const select = e.target;
|
|
361
|
+
const previousLocale = state.get('locale') || 'es';
|
|
362
|
+
const nextLocale = select.value;
|
|
363
|
+
|
|
364
|
+
if (nextLocale === previousLocale) return;
|
|
365
|
+
|
|
366
|
+
select.disabled = true;
|
|
367
|
+
try {
|
|
368
|
+
const result = await api.updateProjectLocale(nextLocale);
|
|
369
|
+
applyLocaleState(result.state);
|
|
370
|
+
flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
|
|
371
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
372
|
+
} catch (err) {
|
|
373
|
+
select.value = previousLocale;
|
|
374
|
+
flash(err.message, 'error');
|
|
375
|
+
} finally {
|
|
376
|
+
select.disabled = false;
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { bindEvents as bind };
|