karajan-code 2.34.0 → 3.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.
Files changed (69) hide show
  1. package/README.md +120 -38
  2. package/bin/kj-rag-mcp.js +75 -0
  3. package/docs/README.es.md +5 -5
  4. package/package.json +22 -17
  5. package/packages/hu-board/package.json +5 -2
  6. package/packages/hu-board/public/app.js +81 -4025
  7. package/packages/hu-board/public/index.html +20 -0
  8. package/packages/hu-board/public/utils/api.js +139 -0
  9. package/packages/hu-board/public/utils/board-view.js +453 -0
  10. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  11. package/packages/hu-board/public/utils/config-editor.js +181 -0
  12. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  13. package/packages/hu-board/public/utils/formatters.js +232 -0
  14. package/packages/hu-board/public/utils/graph-view.js +216 -0
  15. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  16. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  17. package/packages/hu-board/public/utils/log-panel.js +222 -0
  18. package/packages/hu-board/public/utils/modals.js +290 -0
  19. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  20. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  21. package/packages/hu-board/public/utils/project-actions.js +143 -0
  22. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  23. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  24. package/packages/hu-board/public/utils/server-push.js +252 -0
  25. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  26. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  27. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  28. package/packages/hu-board/src/config-yaml.js +14 -0
  29. package/packages/hu-board/src/db.js +15 -2
  30. package/packages/hu-board/src/routes/api.js +27 -9
  31. package/scripts/audit-test-diet.mjs +151 -0
  32. package/src/audit/deterministic-summary.js +20 -0
  33. package/src/audit/harness-scorecard.js +52 -3
  34. package/src/audit/harness-section.js +6 -2
  35. package/src/audit/injection-findings.js +105 -0
  36. package/src/checks/hardware.js +150 -0
  37. package/src/checks/node.js +9 -10
  38. package/src/checks/runner.js +10 -0
  39. package/src/cli/register-meta.js +48 -2
  40. package/src/cli/register-plan.js +10 -1
  41. package/src/commands/clean.js +62 -0
  42. package/src/commands/doctor.js +2 -0
  43. package/src/commands/init.js +41 -1
  44. package/src/commands/plan/generate.js +9 -3
  45. package/src/commands/rag-mcp.js +18 -0
  46. package/src/commands/sync.js +30 -2
  47. package/src/commands/telemetry.js +70 -0
  48. package/src/config/defaults.js +9 -1
  49. package/src/config/schema.js +2 -0
  50. package/src/orchestrator/config-init.js +1 -0
  51. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
  52. package/src/orchestrator/stages/coder-stage.js +11 -2
  53. package/src/orchestrator/stages/reviewer-stage.js +11 -0
  54. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  55. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  56. package/src/prompts/tool-correctness-judge.js +31 -0
  57. package/src/review/git-stash-helper.js +104 -0
  58. package/src/review/tdd-discipline.js +90 -0
  59. package/src/roles/audit-role.js +9 -1
  60. package/src/roles/tool-correctness-judge-role.js +78 -0
  61. package/src/session/mutators.js +4 -0
  62. package/src/sonar/scanner.js +51 -16
  63. package/src/sync/apply-canvas-patch.js +55 -0
  64. package/src/utils/paths.js +12 -0
  65. package/src/utils/quota-simulator.js +56 -0
  66. package/src/utils/repo-cleaner.js +79 -0
  67. package/src/utils/telemetry.js +34 -12
  68. package/src/utils/tool-call-extractor.js +56 -0
  69. package/src/utils/vector-store-orphans.js +89 -0
@@ -0,0 +1,290 @@
1
+ // KJC-TSK-0501 — Modal helpers extracted from app.js (step 2/8).
2
+ // Loaded as a classic script before app.js (after formatters.js) — all
3
+ // declarations hoist to global scope so app.js consumes them transparently.
4
+ // No ES module syntax here on purpose: app.js still has 16+ inline
5
+ // `onclick="closeModal()"` handlers that rely on global functions.
6
+ //
7
+ // Contents:
8
+ // - closeModal(): legacy backdrop modal (#modal-backdrop).
9
+ // - ensureDialog(): singleton <dialog id="app-dialog"> creator.
10
+ // - showError() / showConfirm() / showHelp(): native <dialog> wrappers
11
+ // (project convention forbids alert/confirm/prompt).
12
+ // - Prompt modal: activePromptId state + showPromptModal() +
13
+ // closePromptModalIfMatches() — surface the runner's askQuestion
14
+ // prompts when there's no TTY.
15
+ //
16
+ // Dependencies: `esc()` from formatters.js (already global) and `fetch`.
17
+
18
+ function closeModal() {
19
+ document.getElementById('modal-backdrop').classList.add('hidden');
20
+ }
21
+
22
+ // ---- Native dialog helpers ----
23
+ //
24
+ // Project convention (and common sense): no window.alert / confirm / prompt.
25
+ // These use the browser's built-in modal chrome, steal focus, block script
26
+ // execution, and look foreign on every site. We use <dialog> instead —
27
+ // same blocking semantics without any of the downsides, and it composes
28
+ // with our own CSS variables so it matches the rest of the board.
29
+ //
30
+ // `showError` is the replacement for alert(); `showConfirm` for confirm().
31
+
32
+ function ensureDialog() {
33
+ let dlg = document.getElementById('app-dialog');
34
+ if (dlg) return dlg;
35
+ dlg = document.createElement('dialog');
36
+ dlg.id = 'app-dialog';
37
+ // Position explicitly: showModal() defaults to centered, but when
38
+ // inner content (a wide textarea, the command launcher's form,
39
+ // etc.) pushes past the dialog's natural width Chrome falls back
40
+ // to top-left. Pinning position+transform here makes centering
41
+ // robust regardless of inner content size. We also reset the
42
+ // browser's default 1em margin on <dialog> which would otherwise
43
+ // offset the transform.
44
+ dlg.style.cssText = [
45
+ 'position: fixed',
46
+ 'top: 50%',
47
+ 'left: 50%',
48
+ 'transform: translate(-50%, -50%)',
49
+ 'margin: 0',
50
+ 'border: 1px solid var(--border)',
51
+ 'border-radius: var(--radius-sm)',
52
+ 'padding: 0',
53
+ 'min-width: 320px',
54
+ 'max-width: min(720px, 92vw)',
55
+ 'max-height: 90vh',
56
+ 'overflow: auto',
57
+ 'background: var(--bg-secondary)',
58
+ 'color: var(--text)',
59
+ 'box-shadow: 0 10px 40px rgba(0,0,0,0.45)',
60
+ ].join(';');
61
+ // Inject a backdrop dim rule once. Inline styles can't target the
62
+ // ::backdrop pseudo-element, so we use a one-off <style> tag.
63
+ if (!document.getElementById('app-dialog-style')) {
64
+ const styleEl = document.createElement('style');
65
+ styleEl.id = 'app-dialog-style';
66
+ styleEl.textContent =
67
+ '#app-dialog::backdrop { background: rgba(0,0,0,0.55); }';
68
+ document.head.appendChild(styleEl);
69
+ }
70
+ document.body.appendChild(dlg);
71
+ return dlg;
72
+ }
73
+
74
+ function showError(message, opts = {}) {
75
+ return new Promise((resolve) => {
76
+ const dlg = ensureDialog();
77
+ const title = opts.title || 'Error';
78
+ dlg.innerHTML = `
79
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);
80
+ font-weight:600;color:var(--color-red,#ef4444)">
81
+ ${esc(title)}
82
+ </div>
83
+ <div style="padding:16px 18px;font-size:0.9rem;line-height:1.5;
84
+ white-space:pre-wrap">${esc(message)}</div>
85
+ <div style="padding:12px 18px;border-top:1px solid var(--border);
86
+ text-align:right">
87
+ <button id="app-dialog-ok" class="control-btn"
88
+ style="padding:6px 16px;border:1px solid var(--border);
89
+ background:var(--bg-primary);color:var(--text);
90
+ border-radius:var(--radius-sm);cursor:pointer">
91
+ OK
92
+ </button>
93
+ </div>
94
+ `;
95
+ const done = () => {
96
+ if (dlg.open) dlg.close();
97
+ resolve();
98
+ };
99
+ dlg.addEventListener('close', done, { once: true });
100
+ dlg.querySelector('#app-dialog-ok').addEventListener('click', done, { once: true });
101
+ dlg.showModal();
102
+ });
103
+ }
104
+
105
+ function showHelp() {
106
+ return new Promise((resolve) => {
107
+ const dlg = ensureDialog();
108
+ const sections = [
109
+ ['📋 Board',
110
+ 'Kanban with every HU of the selected project, grouped by status (Pending → Certified → Done). Click a card to open its detail panel; cards highlight when blocked-by another HU.'],
111
+ ['🌐 Graph',
112
+ 'Dependency graph of the selected project — who blocks whom. Useful before scheduling work to spot critical paths or orphan HUs.'],
113
+ ['📊 Dashboard',
114
+ 'Per-project landing: stats grid (total / certified / done) + project list. Click a project card to focus the kanban / graph on that project.'],
115
+ ['📚 Sessions',
116
+ 'Every kj run that has touched the board, newest first. Filter by project; click a session to inspect its iterations, commits and checkpoints.'],
117
+ ['⚙️ Pipeline',
118
+ 'Live observability of pipeline runs (Karajan v2.7+): stage timings, agent calls, decision logs. Opens in a separate page since the data is per-run, not per-project.'],
119
+ ];
120
+ const sectionHtml = sections
121
+ .map(([title, body]) => `
122
+ <div style="padding:10px 0;border-bottom:1px solid var(--border)">
123
+ <div style="font-weight:600;color:var(--accent-purple);margin-bottom:4px">${esc(title)}</div>
124
+ <div style="font-size:0.85rem;line-height:1.5;color:var(--text-secondary)">${esc(body)}</div>
125
+ </div>`)
126
+ .join('');
127
+ dlg.innerHTML = `
128
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:600">
129
+ HU Board — what does each view do?
130
+ </div>
131
+ <div style="padding:8px 18px 4px 18px">${sectionHtml}</div>
132
+ <div style="padding:10px 18px;border-top:1px solid var(--border);
133
+ font-size:0.78rem;color:var(--text-muted)">
134
+ Tip: hover any tab in the header for ~1 second to see a one-line summary.
135
+ </div>
136
+ <div style="padding:12px 18px;border-top:1px solid var(--border);text-align:right">
137
+ <button id="app-dialog-close" class="control-btn"
138
+ style="padding:6px 16px;border:1px solid var(--border);
139
+ background:var(--bg-primary);color:var(--text);
140
+ border-radius:var(--radius-sm);cursor:pointer">
141
+ Close
142
+ </button>
143
+ </div>
144
+ `;
145
+ const done = () => {
146
+ if (dlg.open) dlg.close();
147
+ resolve();
148
+ };
149
+ dlg.addEventListener('close', done, { once: true });
150
+ dlg.querySelector('#app-dialog-close').addEventListener('click', done, { once: true });
151
+ dlg.showModal();
152
+ });
153
+ }
154
+
155
+ function showConfirm(message, opts = {}) {
156
+ return new Promise((resolve) => {
157
+ const dlg = ensureDialog();
158
+ const title = opts.title || 'Confirm';
159
+ const okLabel = opts.okLabel || 'OK';
160
+ const cancelLabel = opts.cancelLabel || 'Cancel';
161
+ const okColor = opts.destructive ? 'var(--color-red,#ef4444)' : 'var(--color-green)';
162
+ dlg.innerHTML = `
163
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:600">
164
+ ${esc(title)}
165
+ </div>
166
+ <div style="padding:16px 18px;font-size:0.9rem;line-height:1.5;
167
+ white-space:pre-wrap">${esc(message)}</div>
168
+ <div style="padding:12px 18px;border-top:1px solid var(--border);
169
+ display:flex;justify-content:flex-end;gap:8px">
170
+ <button id="app-dialog-cancel" class="control-btn"
171
+ style="padding:6px 14px;border:1px solid var(--border);
172
+ background:var(--bg-primary);color:var(--text);
173
+ border-radius:var(--radius-sm);cursor:pointer">
174
+ ${esc(cancelLabel)}
175
+ </button>
176
+ <button id="app-dialog-ok" class="control-btn"
177
+ style="padding:6px 14px;border:none;background:${okColor};
178
+ color:#fff;border-radius:var(--radius-sm);cursor:pointer">
179
+ ${esc(okLabel)}
180
+ </button>
181
+ </div>
182
+ `;
183
+ let answer = false;
184
+ const finish = () => {
185
+ if (dlg.open) dlg.close();
186
+ resolve(answer);
187
+ };
188
+ dlg.addEventListener('close', finish, { once: true });
189
+ dlg.querySelector('#app-dialog-ok').addEventListener('click', () => { answer = true; finish(); }, { once: true });
190
+ dlg.querySelector('#app-dialog-cancel').addEventListener('click', () => { answer = false; finish(); }, { once: true });
191
+ dlg.showModal();
192
+ });
193
+ }
194
+
195
+ // ---- Prompt modal ----
196
+ //
197
+ // The runner publishes a prompt JSON file when it needs an answer
198
+ // (Solomon escalations, max-iterations decisions, anything wired to
199
+ // askQuestion) and there's no TTY to ask through. The board surfaces
200
+ // the prompt as a modal dialog with a text input. When the user
201
+ // answers, we POST back; the runner sees the answer file and
202
+ // resolves.
203
+
204
+ let activePromptId = null;
205
+
206
+ function showPromptModal(prompt) {
207
+ if (!prompt?.promptId) return;
208
+ if (activePromptId === prompt.promptId) return;
209
+ activePromptId = prompt.promptId;
210
+ const dlg = ensureDialog();
211
+ const ctxBlock = prompt.context
212
+ ? `<details style="margin-top:8px">
213
+ <summary style="cursor:pointer;color:var(--text-muted);font-size:0.85rem">Context</summary>
214
+ <pre style="white-space:pre-wrap;font-size:0.78rem;background:var(--bg-primary);padding:8px;border-radius:var(--radius-sm);overflow:auto;max-height:240px">${esc(JSON.stringify(prompt.context, null, 2))}</pre>
215
+ </details>`
216
+ : '';
217
+ dlg.innerHTML = `
218
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:600;display:flex;align-items:center;gap:10px">
219
+ <span>❓ Karajan needs an answer</span>
220
+ ${prompt.sessionId ? `<span style="font-family:var(--font-mono, monospace);font-size:0.75rem;color:var(--text-muted)">${esc(prompt.sessionId)}</span>` : ''}
221
+ </div>
222
+ <div style="padding:14px 18px">
223
+ <div style="white-space:pre-wrap;font-size:0.95rem;line-height:1.55">${esc(prompt.question)}</div>
224
+ ${ctxBlock}
225
+ <textarea id="prompt-answer" rows="3"
226
+ placeholder="Type your answer (or 'stop' to abort the run)"
227
+ style="margin-top:12px;width:100%;padding:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);font-family:inherit;font-size:0.9rem;line-height:1.5;resize:vertical;box-sizing:border-box"></textarea>
228
+ </div>
229
+ <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 18px;border-top:1px solid var(--border);gap:8px">
230
+ <span style="font-size:0.75rem;color:var(--text-muted)">
231
+ The runner is blocked waiting for this answer. Closing the dialog without answering aborts the prompt (run stops).
232
+ </span>
233
+ <div style="display:flex;gap:8px">
234
+ <button id="prompt-stop" type="button" class="control-btn"
235
+ style="padding:6px 14px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">
236
+ Stop
237
+ </button>
238
+ <button id="prompt-send" type="button" class="control-btn"
239
+ style="padding:6px 14px;border:none;background:var(--color-green);color:#fff;border-radius:var(--radius-sm);cursor:pointer;font-weight:600">
240
+ Send
241
+ </button>
242
+ </div>
243
+ </div>
244
+ `;
245
+
246
+ const send = async (rawAnswer) => {
247
+ const answer = String(rawAnswer ?? '');
248
+ try {
249
+ await fetch(`/api/prompts/${encodeURIComponent(prompt.promptId)}/answer`, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({ answer }),
253
+ });
254
+ } catch (err) {
255
+ await showError(`Could not deliver answer: ${err.message}`, { title: 'Prompt delivery failed' });
256
+ return;
257
+ }
258
+ activePromptId = null;
259
+ if (dlg.open) dlg.close();
260
+ };
261
+
262
+ dlg.querySelector('#prompt-send').addEventListener('click', () => {
263
+ send(dlg.querySelector('#prompt-answer').value.trim());
264
+ });
265
+ dlg.querySelector('#prompt-stop').addEventListener('click', () => send('stop'));
266
+
267
+ // Esc / backdrop click also count as Stop so the user can't accidentally
268
+ // leave the runner blocked.
269
+ dlg.addEventListener('close', () => {
270
+ if (activePromptId === prompt.promptId) {
271
+ activePromptId = null;
272
+ // Best-effort stop; ignore errors if the runner already moved on.
273
+ fetch(`/api/prompts/${encodeURIComponent(prompt.promptId)}/answer`, {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ answer: 'stop' }),
277
+ }).catch(() => {});
278
+ }
279
+ }, { once: true });
280
+
281
+ dlg.showModal();
282
+ setTimeout(() => dlg.querySelector('#prompt-answer')?.focus(), 50);
283
+ }
284
+
285
+ function closePromptModalIfMatches(promptId) {
286
+ if (activePromptId !== promptId) return;
287
+ activePromptId = null;
288
+ const dlg = document.getElementById('app-dialog');
289
+ if (dlg && dlg.open) dlg.close();
290
+ }
@@ -0,0 +1,53 @@
1
+ // KJC-TSK-0501 step 7/8 — Plan rollup banner (post-run summary).
2
+ //
3
+ // Classic script (no exports). Loaded by index.html before app.js so
4
+ // the function hoists into the script-level lexical environment and
5
+ // app.js (handleRoute → kanban view) can call renderPlanRollup().
6
+ //
7
+ // Globals consumed from earlier scripts / app.js:
8
+ // - esc() (app.js)
9
+ // - formatDuration() (utils/formatters.js)
10
+
11
+ /**
12
+ * Compact "what just happened" banner shown above the kanban when the
13
+ * most recently finished plan has an outcome. Renders status colour,
14
+ * counts (done / failed / blocked), duration, and any PR links.
15
+ * No-op when the banner slot isn't mounted or when no plan has
16
+ * finished yet for this project.
17
+ */
18
+ async function renderPlanRollup(projectId) {
19
+ const slot = document.getElementById('plan-rollup-banner');
20
+ if (!slot) return;
21
+ let data;
22
+ try {
23
+ const r = await fetch(`/api/projects/${encodeURIComponent(projectId)}/plans-outcome`);
24
+ if (!r.ok) return;
25
+ data = await r.json();
26
+ } catch { return; }
27
+ const finished = (data.plans || []).filter(p => p.outcome);
28
+ if (finished.length === 0) { slot.innerHTML = ''; return; }
29
+ // Aggregate the most recent plan first — non-tech user usually
30
+ // cares about "what happened just now", not historical sums.
31
+ finished.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
32
+ const recent = finished[0];
33
+ const o = recent.outcome;
34
+ const c = o.counts || {};
35
+ const summaryColor = o.status === 'done' ? 'var(--color-green)'
36
+ : o.status === 'failed' ? 'var(--color-red,#ef4444)'
37
+ : 'var(--color-yellow,#eab308)';
38
+ const headline = o.status === 'done' ? 'Plan finalizado correctamente'
39
+ : o.status === 'failed' ? 'Plan terminado con errores'
40
+ : 'Plan terminado parcialmente';
41
+ const durationText = o.duration_ms != null ? formatDuration(o.duration_ms) : '—';
42
+ slot.innerHTML = `
43
+ <div style="margin:8px 0;padding:10px 14px;background:var(--bg-primary);border:1px solid var(--border);border-left:4px solid ${summaryColor};border-radius:var(--radius-sm);font-size:0.85rem;display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
44
+ <span style="color:${summaryColor};font-weight:700;">${esc(headline)}</span>
45
+ <span style="color:var(--text-muted);">${esc(recent.name || recent.planId)}</span>
46
+ <span style="color:var(--text);">✓ ${c.done || 0} hechas</span>
47
+ ${c.failed ? `<span style="color:var(--color-red,#ef4444)">✗ ${c.failed} fallidas</span>` : ''}
48
+ ${c.blocked ? `<span style="color:var(--color-yellow,#eab308)">⏸ ${c.blocked} bloqueadas</span>` : ''}
49
+ <span style="color:var(--text-muted);">⏱ ${esc(durationText)}</span>
50
+ ${(o.prs || []).length > 0 ? `<span style="color:var(--text-muted);">${o.prs.length} PR(s):</span> ${o.prs.map(u => `<a href="${esc(u)}" target="_blank" rel="noopener" style="color:var(--color-green);">abrir</a>`).join(' · ')}` : ''}
51
+ </div>
52
+ `;
53
+ }
@@ -0,0 +1,89 @@
1
+ // KJC-TSK-0501 step 7/8 — Preflight panel (compact run-readiness banner).
2
+ //
3
+ // Classic script (no exports). Loaded by index.html before app.js so
4
+ // the function hoists into the script-level lexical environment and
5
+ // app.js (handleRoute → kanban view) can call renderPreflightPanel().
6
+ //
7
+ // The companion helpers fetchPreflight(), preflightStatusIcon(),
8
+ // preflightStatusColor() and the mutable `preflightCache` are kept in
9
+ // app.js because the confirmRunWithPreflight() modal also reads them;
10
+ // extracting both would force a forward-reference dance we don't need
11
+ // at this stage.
12
+ //
13
+ // Globals consumed from earlier scripts / app.js:
14
+ // - esc() (app.js)
15
+ // - fetchPreflight() (app.js)
16
+ // - preflightStatusIcon() (app.js)
17
+ // - preflightStatusColor() (app.js)
18
+ // - preflightCache (mutable, app.js — we assign to it)
19
+
20
+ /**
21
+ * Render the compact preflight banner above the kanban. Shows a single
22
+ * summary chip (ok / warn / fail) plus a click-to-expand grid of every
23
+ * deterministic check the backend ran. No-op if the panel slot isn't
24
+ * in the DOM yet (the kanban hasn't mounted).
25
+ */
26
+ async function renderPreflightPanel(projectId) {
27
+ const panel = document.getElementById('preflight-panel');
28
+ if (!panel) return;
29
+ const data = await fetchPreflight(projectId);
30
+ preflightCache = data;
31
+ if (!data) {
32
+ panel.innerHTML = `<div style="font-size:0.8rem;color:var(--text-muted);padding:6px 10px;">Estado del proyecto: no disponible.</div>`;
33
+ return;
34
+ }
35
+ const blockerCount = data.blockers?.length || 0;
36
+ const warningCount = data.warnings?.length || 0;
37
+ const okCount = (data.checks || []).filter(c => c.status === 'ok').length;
38
+ let summaryColor, summaryIcon, summaryText;
39
+ if (blockerCount > 0) {
40
+ summaryColor = 'var(--color-red,#ef4444)';
41
+ summaryIcon = '✗';
42
+ summaryText = `${blockerCount} problema(s) bloqueante(s) — la ejecución no funcionará correctamente`;
43
+ } else if (warningCount > 0) {
44
+ summaryColor = 'var(--color-yellow,#eab308)';
45
+ summaryIcon = '⚠';
46
+ summaryText = `${warningCount} aviso(s) — la ejecución funcionará pero con limitaciones`;
47
+ } else {
48
+ summaryColor = 'var(--color-green)';
49
+ summaryIcon = '✓';
50
+ summaryText = `Todo listo · ${okCount} comprobaciones OK`;
51
+ }
52
+
53
+ // Compact header (always visible) + collapsed details (toggleable).
54
+ panel.innerHTML = `
55
+ <div class="preflight-panel__header"
56
+ style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-primary);border:1px solid var(--border);border-left:3px solid ${summaryColor};border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem;"
57
+ data-toggle>
58
+ <span style="color:${summaryColor};font-weight:700;font-size:1rem;">${summaryIcon}</span>
59
+ <span style="color:var(--text);">${esc(summaryText)}</span>
60
+ <span style="margin-left:auto;color:var(--text-muted);font-size:0.78rem;" data-arrow>▼ ver detalles</span>
61
+ </div>
62
+ <div class="preflight-panel__details" style="display:none;margin-top:6px;padding:10px 12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);">
63
+ <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:8px;">
64
+ ${(data.checks || []).map((c) => `
65
+ <div style="display:flex;align-items:flex-start;gap:8px;padding:6px 8px;background:var(--bg-secondary,var(--bg));border-radius:var(--radius-sm);"
66
+ title="${esc(c.consequence || '')}">
67
+ <span style="color:${preflightStatusColor(c.status)};font-weight:700;font-size:0.95rem;flex-shrink:0;">${preflightStatusIcon(c.status)}</span>
68
+ <div style="flex:1;min-width:0;">
69
+ <div style="font-size:0.8rem;font-weight:600;color:var(--text);">${esc(c.label)}</div>
70
+ <div style="font-size:0.72rem;color:var(--text-muted);word-break:break-all;">${esc(c.detail || '')}</div>
71
+ ${c.consequence ? `<div style="font-size:0.7rem;color:${preflightStatusColor(c.status)};margin-top:2px;">${esc(c.consequence)}</div>` : ''}
72
+ </div>
73
+ </div>
74
+ `).join('')}
75
+ </div>
76
+ </div>
77
+ `;
78
+ // Toggle expand/collapse without rerender.
79
+ const header = panel.querySelector('[data-toggle]');
80
+ const details = panel.querySelector('.preflight-panel__details');
81
+ const arrow = panel.querySelector('[data-arrow]');
82
+ if (header && details && arrow) {
83
+ header.addEventListener('click', () => {
84
+ const open = details.style.display !== 'none';
85
+ details.style.display = open ? 'none' : 'block';
86
+ arrow.textContent = open ? '▼ ver detalles' : '▲ ocultar detalles';
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,143 @@
1
+ // KJC-TSK-0501 step 8b/8 — Project-level dialogs (rename + outcome).
2
+ //
3
+ // Classic script (no exports). Loaded by index.html before app.js so the
4
+ // `window.*` assignments are in place by the time the kanban mounts.
5
+ // Inline `onclick="..."` attributes (rename ✎ button in the header,
6
+ // outcome chip in board-view.js) resolve these handlers via `window`.
7
+ //
8
+ // Globals consumed from earlier scripts / app.js:
9
+ // - esc() (app.js)
10
+ // - ensureDialog() (app.js)
11
+ // - formatDuration() (utils/formatters.js)
12
+ // - projectNameCache (mutable in app.js — assignment crosses
13
+ // scripts transparently)
14
+ // - populateProjectSelect() (app.js)
15
+ // - renderBoard() (app.js)
16
+
17
+ /**
18
+ * PR-G: rename a project from the header ✎ button. Opens a small
19
+ * dialog with the current name pre-filled, validates length, PUTs
20
+ * to /api/projects/:id/name, and re-renders the board so the new
21
+ * name shows up everywhere (header, dropdown, picker).
22
+ */
23
+ window.renameProjectModal = function renameProjectModal(projectId, currentName) {
24
+ const dlg = document.getElementById('app-dialog') || ensureDialog();
25
+ const safeCurrent = String(currentName || '').replace(/&#39;/g, "'");
26
+ dlg.innerHTML = `
27
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:700;display:flex;align-items:center;gap:10px;">
28
+ <span style="font-size:1.1rem;">✎</span>
29
+ <span>Renombrar proyecto</span>
30
+ </div>
31
+ <div style="padding:14px 18px;">
32
+ <label for="rename-input" style="display:block;font-size:0.85rem;color:var(--text);margin-bottom:6px;">
33
+ Nombre del proyecto
34
+ </label>
35
+ <input type="text" id="rename-input" value="${esc(safeCurrent)}" maxlength="120"
36
+ style="width:100%;padding:8px 10px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);font-size:0.95rem;" />
37
+ <p style="margin:8px 0 0;font-size:0.75rem;color:var(--text-muted);">
38
+ Se actualizará el campo <code>name</code> en cada plan JSON de este proyecto y la fila en la base de datos del board.
39
+ </p>
40
+ <p id="rename-error" style="margin:8px 0 0;font-size:0.78rem;color:var(--color-red,#ef4444);display:none;"></p>
41
+ </div>
42
+ <div style="padding:12px 18px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;">
43
+ <button id="rename-cancel" style="padding:8px 16px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;">Cancelar</button>
44
+ <button id="rename-save" style="padding:8px 16px;background:var(--color-green);border:none;color:#fff;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;">Guardar</button>
45
+ </div>
46
+ `;
47
+ if (typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal();
48
+ const input = dlg.querySelector('#rename-input');
49
+ const errorEl = dlg.querySelector('#rename-error');
50
+ input?.focus();
51
+ input?.select();
52
+ dlg.querySelector('#rename-cancel')?.addEventListener('click', () => { try { dlg.close(); } catch { /* ignore */ } }, { once: true });
53
+ dlg.querySelector('#rename-save')?.addEventListener('click', async () => {
54
+ const newName = input?.value?.trim() || '';
55
+ if (!newName) { errorEl.style.display = 'block'; errorEl.textContent = 'El nombre no puede estar vacío.'; return; }
56
+ if (newName === safeCurrent) { try { dlg.close(); } catch { /* ignore */ } return; }
57
+ try {
58
+ const r = await fetch(`/api/projects/${encodeURIComponent(projectId)}/name`, {
59
+ method: 'PUT',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({ name: newName }),
62
+ });
63
+ if (!r.ok) {
64
+ const body = await r.json().catch(() => ({}));
65
+ errorEl.style.display = 'block';
66
+ errorEl.textContent = body?.error || `HTTP ${r.status}`;
67
+ return;
68
+ }
69
+ // Update the cached display name + refresh the board so the
70
+ // header, dropdown and project picker all show the new value.
71
+ projectNameCache[projectId] = newName;
72
+ try { dlg.close(); } catch { /* ignore */ }
73
+ await populateProjectSelect();
74
+ await renderBoard();
75
+ } catch (err) {
76
+ errorEl.style.display = 'block';
77
+ errorEl.textContent = err.message || String(err);
78
+ }
79
+ }, { once: true });
80
+ };
81
+
82
+ /**
83
+ * Open the outcome detail modal for a single HU. The chip on each
84
+ * card calls this with the JSON-stringified outcome (escaped for the
85
+ * inline onclick attribute) — we parse and render the breakdown.
86
+ *
87
+ * Surfaces the per-HU + plan-level execution outcome the orchestrator
88
+ * stamps on the plan JSON. Plain Spanish, no jargon.
89
+ */
90
+ window.showOutcomeModal = function showOutcomeModal(escapedJson) {
91
+ let outcome;
92
+ try { outcome = JSON.parse(String(escapedJson).replace(/&#39;/g, "'")); }
93
+ catch { return; }
94
+ const dlg = document.getElementById('app-dialog') || ensureDialog();
95
+ const status = outcome.status || 'desconocido';
96
+ const headerColor = status === 'done' ? 'var(--color-green)'
97
+ : status === 'failed' ? 'var(--color-red,#ef4444)'
98
+ : 'var(--color-yellow,#eab308)';
99
+ const headerLabel = status === 'done' ? 'HU completada'
100
+ : status === 'failed' ? 'HU fallida'
101
+ : status === 'blocked' ? 'HU bloqueada'
102
+ : `HU · ${status}`;
103
+ const durationText = outcome.duration_ms != null ? formatDuration(outcome.duration_ms) : '—';
104
+ const blockersHtml = (outcome.blockers || []).length === 0 ? '' : `
105
+ <div style="margin-top:10px;">
106
+ <div style="font-weight:600;color:var(--color-red,#ef4444);font-size:0.85rem;">Problemas detectados</div>
107
+ <ul style="margin:4px 0 0 18px;padding:0;color:var(--text);">
108
+ ${outcome.blockers.map(b => `<li style="font-size:0.8rem;margin-top:4px;">${esc(b)}</li>`).join('')}
109
+ </ul>
110
+ </div>
111
+ `;
112
+ const commitsHtml = (outcome.commits || []).length === 0 ? '' : `
113
+ <div style="margin-top:10px;">
114
+ <div style="font-weight:600;color:var(--text);font-size:0.85rem;">Commits</div>
115
+ <ul style="margin:4px 0 0 18px;padding:0;color:var(--text-muted);">
116
+ ${outcome.commits.map(c => `<li style="font-family:var(--font-mono,monospace);font-size:0.78rem;">${esc(c)}</li>`).join('')}
117
+ </ul>
118
+ </div>
119
+ `;
120
+ dlg.innerHTML = `
121
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:700;display:flex;align-items:center;gap:10px;background:${headerColor};color:#000;">
122
+ <span style="font-size:1.2rem;">📄</span>
123
+ <span>${esc(headerLabel)}</span>
124
+ </div>
125
+ <div style="padding:14px 18px;max-height:65vh;overflow:auto;">
126
+ <p style="margin:0 0 10px;color:var(--text);">${esc(outcome.summary || '(sin resumen disponible)')}</p>
127
+ <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:8px;font-size:0.8rem;">
128
+ <div><strong>Iteraciones</strong>: ${outcome.iterations ?? '—'}</div>
129
+ <div><strong>Duración</strong>: ${durationText}</div>
130
+ <div><strong>Rama</strong>: <span style="font-family:var(--font-mono,monospace)">${esc(outcome.branch || '—')}</span></div>
131
+ <div><strong>Pull Request</strong>: ${outcome.pr_url ? `<a href="${esc(outcome.pr_url)}" target="_blank" rel="noopener">abrir</a>` : '—'}</div>
132
+ <div><strong>Terminada</strong>: ${esc(outcome.finishedAt || '—')}</div>
133
+ </div>
134
+ ${blockersHtml}
135
+ ${commitsHtml}
136
+ </div>
137
+ <div style="padding:12px 18px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;">
138
+ <button id="outcome-close" style="padding:8px 16px;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;">Cerrar</button>
139
+ </div>
140
+ `;
141
+ if (typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal();
142
+ dlg.querySelector('#outcome-close')?.addEventListener('click', () => { try { dlg.close(); } catch { /* ignore */ } }, { once: true });
143
+ };