karajan-code 3.0.0 → 3.2.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 +14 -6
- package/docs/README.es.md +1 -1
- package/package.json +8 -2
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +21 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +462 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +273 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +93 -2
- package/packages/hu-board/src/format.js +79 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +43 -10
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +4 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/audit-test-diet.mjs +151 -0
- package/scripts/git-hooks/post-merge +13 -2
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/brain/standby-scheduler.js +10 -92
- package/src/brain/standby-store.js +12 -168
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/runner.js +10 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +36 -2
- package/src/cli/register-pipeline.js +3 -0
- package/src/cli/register-plan.js +10 -1
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/commands/sync.js +30 -2
- package/src/config/defaults.js +5 -0
- package/src/config/schema.js +2 -0
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/hu-sub-pipeline.js +52 -8
- package/src/orchestrator/stages/coder-stage.js +8 -2
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/plan/plan-hu-ops.js +16 -338
- package/src/plan/plan-id.js +4 -45
- package/src/plan/plan-validation.js +4 -82
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/rag/vec-store.js +17 -253
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +11 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/cli-ask-question.js +76 -0
- package/src/utils/doc-links.js +2 -0
- package/src/utils/os-detect.js +10 -0
- package/src/utils/paths.js +14 -111
- package/src/utils/port-check.js +6 -45
- package/src/utils/process.js +4 -188
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/run-registry.js +9 -116
- package/src/utils/shared-paths.js +12 -41
- package/src/utils/squeezr-detect.js +18 -0
- package/src/utils/squeezr-install.js +37 -0
- package/src/utils/tool-call-extractor.js +56 -0
- 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(/'/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(/'/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
|
+
};
|