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,224 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 8a/8 — HU action handlers (per-card and per-modal actions).
|
|
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 emitted by board-view.js / dashboard-
|
|
6
|
+
// view.js / story-detail-view.js resolve these handlers via `window`.
|
|
7
|
+
//
|
|
8
|
+
// Globals consumed from earlier scripts / app.js:
|
|
9
|
+
// - showConfirm() (utils/modals.js)
|
|
10
|
+
// - showError() (utils/modals.js)
|
|
11
|
+
// - closeModal() (app.js → window.closeModal)
|
|
12
|
+
// - renderBoard() (app.js)
|
|
13
|
+
// - openGenericLogPanel() (utils/log-panel.js)
|
|
14
|
+
// - lastOpenedLog (mutable in app.js — assignment crosses scripts
|
|
15
|
+
// transparently because both share the same realm)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PR4: launch a single HU from the per-card ▶ button. Confirms with
|
|
19
|
+
* the user (so an accidental click doesn't kick a run) and POSTs to
|
|
20
|
+
* the new /api/hus/:huId/run endpoint, which resolves the planId
|
|
21
|
+
* server-side from the SQLite row.
|
|
22
|
+
*
|
|
23
|
+
* Exposed on `window` because the inline onclick on the card calls it.
|
|
24
|
+
*/
|
|
25
|
+
window.runSingleHuFromCard = async function runSingleHuFromCard(huId, title) {
|
|
26
|
+
const friendlyTitle = (title || huId).replace(/'/g, "'");
|
|
27
|
+
const ok = await showConfirm(
|
|
28
|
+
`¿Lanzar solo esta HU?\n\n${friendlyTitle}\n\nKarajan ejecutará únicamente esta HU; las demás del plan no se tocarán.`,
|
|
29
|
+
{ title: 'Lanzar HU individual', okLabel: 'Lanzar', cancelLabel: 'Cancelar' }
|
|
30
|
+
);
|
|
31
|
+
if (!ok) return;
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`/api/hus/${encodeURIComponent(huId)}/run`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({}),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
40
|
+
await showError(msg, { title: 'No se pudo lanzar la HU' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const body = await res.json();
|
|
44
|
+
// Surface the log path so the user can tail it from the View log button
|
|
45
|
+
// AND auto-open the viewer so the click produces visible feedback. Without
|
|
46
|
+
// the open, the user clicks Lanzar, sees nothing change, and concludes the
|
|
47
|
+
// run never started — the run IS started, the log IS being written, the
|
|
48
|
+
// UI just wasn't showing it.
|
|
49
|
+
//
|
|
50
|
+
// The tail URL must be derived from body.logPath, not constructed from
|
|
51
|
+
// huId. When `kj run --plan ... --hu <id>` is launched, runPlan writes to
|
|
52
|
+
// <runsDir>/<planId>--hu-<localHuId>.log
|
|
53
|
+
// — not <runsDir>/hu-<huId>.log. The previous code guessed the wrong
|
|
54
|
+
// filename and the viewer would have hit a non-existent file even if
|
|
55
|
+
// it had been opened.
|
|
56
|
+
if (body.logPath) {
|
|
57
|
+
const logBasename = body.logPath.split('/').pop().replace(/\.log$/, '');
|
|
58
|
+
const tailUrl = (offset) => `/api/runs/${encodeURIComponent(logBasename)}/log?offset=${offset || 0}`;
|
|
59
|
+
lastOpenedLog = { id: `hu-${huId}`, label: `HU ${huId}`, tailUrl };
|
|
60
|
+
openGenericLogPanel({ id: logBasename, label: `HU ${huId}`, tailUrl });
|
|
61
|
+
}
|
|
62
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
63
|
+
await renderBoard();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
await showError(err.message || String(err), { title: 'Fallo al lanzar la HU' });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* KJC-TSK-0394 step 2: devolver una HU al estado `pending` desde
|
|
71
|
+
* cualquier estado no-terminal (coding/reviewing/blocked zombi) o
|
|
72
|
+
* terminal (done/failed) que el usuario quiera relanzar limpio.
|
|
73
|
+
*
|
|
74
|
+
* No tocamos el campo `result`: si una HU acaba en done+fail y el
|
|
75
|
+
* usuario hace Reset → status: pending, result: fail (badge ✗ persiste
|
|
76
|
+
* como historial). El siguiente run sobreescribirá result cuando
|
|
77
|
+
* termine.
|
|
78
|
+
*
|
|
79
|
+
* El backend ya acepta {status: 'pending'} en ALLOWED_STORY_STATUSES
|
|
80
|
+
* (ver routes/api.js); plan-mutations::setHuStatus refleja el cambio
|
|
81
|
+
* al fichero del plan y resincroniza la BBDD.
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* Cambio libre de status desde el dropdown del modal. PATCH al backend
|
|
85
|
+
* con el status nuevo + confirmación (algunos targets como `done` /
|
|
86
|
+
* `failed` son blast-radius alto: no se puede deshacer sin Reset).
|
|
87
|
+
*
|
|
88
|
+
* El propio backend valida que `newStatus` esté en ALLOWED_STORY_
|
|
89
|
+
* STATUSES, así que si el dropdown trae basura el servidor rechaza con
|
|
90
|
+
* 400 — no necesitamos re-validar aquí.
|
|
91
|
+
*/
|
|
92
|
+
window.changeHuStatusFromModal = async function changeHuStatusFromModal(storyId, newStatus, oldStatus) {
|
|
93
|
+
// Opción "Cambiar a…" placeholder: el usuario abrió el dropdown sin
|
|
94
|
+
// elegir nada. Selecciona el placeholder otra vez para no quedar en
|
|
95
|
+
// un estado ambiguo.
|
|
96
|
+
if (!newStatus) return;
|
|
97
|
+
const select = document.getElementById('hu-status-select');
|
|
98
|
+
const ok = await showConfirm(
|
|
99
|
+
`Cambiar el status de esta HU de "${oldStatus}" a "${newStatus}"?\n\nEl plan file y el board se actualizan al instante. El campo result (badge ✓/✗) se conserva como historial.`,
|
|
100
|
+
{ title: 'Cambiar status', okLabel: 'Cambiar', cancelLabel: 'Cancelar' }
|
|
101
|
+
);
|
|
102
|
+
if (!ok) { if (select) select.value = ''; return; }
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`/api/stories/${encodeURIComponent(storyId)}`, {
|
|
105
|
+
method: 'PATCH',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ status: newStatus }),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
111
|
+
await showError(msg, { title: 'No se pudo cambiar el status' });
|
|
112
|
+
if (select) select.value = '';
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
closeModal();
|
|
116
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
117
|
+
await renderBoard();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
await showError(err.message || String(err), { title: 'Fallo al cambiar el status' });
|
|
120
|
+
if (select) select.value = '';
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// KJC-TSK-0406: persist coder_model + reviewer_model overrides from the
|
|
125
|
+
// modal. Vacío → null (re-asignación automática por triage en siguiente
|
|
126
|
+
// run). Cada modelo es independiente.
|
|
127
|
+
window.saveHuModels = async function saveHuModels(storyId) {
|
|
128
|
+
const coderInput = document.getElementById('hu-coder-model');
|
|
129
|
+
const reviewerInput = document.getElementById('hu-reviewer-model');
|
|
130
|
+
const patch = {
|
|
131
|
+
coder_model: coderInput?.value?.trim() || null,
|
|
132
|
+
reviewer_model: reviewerInput?.value?.trim() || null,
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(`/api/stories/${encodeURIComponent(storyId)}`, {
|
|
136
|
+
method: 'PATCH',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify(patch),
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
142
|
+
await showError(msg, { title: 'No se pudieron guardar los modelos' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
146
|
+
await renderBoard();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
await showError(err.message || String(err), { title: 'Fallo guardando modelos' });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// KJC-PRP-0002 PR6: persist `hu.assignee` (free-form handle of whoever owns
|
|
153
|
+
// this HU on a team-shared board). Empty string → null so the API knows to
|
|
154
|
+
// clear, not store "". The modal only renders the input when the project is
|
|
155
|
+
// `is_shared = 1`, so non-team boards never trigger this path.
|
|
156
|
+
window.saveHuAssignee = async function saveHuAssignee(storyId) {
|
|
157
|
+
const input = document.getElementById('hu-assignee');
|
|
158
|
+
const value = input?.value?.trim() || null;
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch(`/api/stories/${encodeURIComponent(storyId)}`, {
|
|
161
|
+
method: 'PATCH',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ assignee: value }),
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
167
|
+
await showError(msg, { title: 'No se pudo guardar el asignado' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
171
|
+
await renderBoard();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
await showError(err.message || String(err), { title: 'Fallo guardando asignado' });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// KJC-TSK-0408: deshace los cambios de una HU restaurando el snapshot
|
|
178
|
+
// git tomado pre-run. ATENCIÓN: es destructivo (git reset --hard) —
|
|
179
|
+
// confirmamos siempre antes de invocar.
|
|
180
|
+
window.undoHuChanges = async function undoHuChanges(storyId) {
|
|
181
|
+
const ok = await showConfirm(
|
|
182
|
+
'⚠️ Esta acción es destructiva.\n\nSe descartan los cambios de ficheros que esta HU hizo durante su última ejecución y se restaura el estado pre-run. La HU vuelve a pending y podrás relanzarla con otros modelos.\n\n¿Continuar?',
|
|
183
|
+
{ title: 'Deshacer cambios de la HU', okLabel: 'Deshacer', cancelLabel: 'Cancelar' }
|
|
184
|
+
);
|
|
185
|
+
if (!ok) return;
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(`/api/stories/${encodeURIComponent(storyId)}/undo`, { method: 'POST' });
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
190
|
+
await showError(msg, { title: 'No se pudo deshacer la HU' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
closeModal();
|
|
194
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
195
|
+
await renderBoard();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
await showError(err.message || String(err), { title: 'Fallo al deshacer la HU' });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
window.resetHuToPending = async function resetHuToPending(storyId) {
|
|
202
|
+
const ok = await showConfirm(
|
|
203
|
+
'¿Devolver esta HU a pending?\n\nEl estado se cambia a pending y se podrá relanzar. El resultado de la última ejecución (✓/✗/~) se conserva como historial hasta que vuelvas a ejecutarla.',
|
|
204
|
+
{ title: 'Reset HU', okLabel: 'Reset', cancelLabel: 'Cancelar' }
|
|
205
|
+
);
|
|
206
|
+
if (!ok) return;
|
|
207
|
+
try {
|
|
208
|
+
const res = await fetch(`/api/stories/${encodeURIComponent(storyId)}`, {
|
|
209
|
+
method: 'PATCH',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({ status: 'pending' }),
|
|
212
|
+
});
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
const msg = (await res.json().catch(() => ({}))).error || `HTTP ${res.status}`;
|
|
215
|
+
await showError(msg, { title: 'No se pudo resetear la HU' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
closeModal();
|
|
219
|
+
await fetch('/api/sync', { method: 'POST' }).catch(() => {});
|
|
220
|
+
await renderBoard();
|
|
221
|
+
} catch (err) {
|
|
222
|
+
await showError(err.message || String(err), { title: 'Fallo al resetear la HU' });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 8h/8 — Initialization listeners + boot sequence.
|
|
2
|
+
//
|
|
3
|
+
// Classic script (no exports). Loaded by index.html AFTER all other
|
|
4
|
+
// utils/*.js (including utils/server-push.js) so every globally
|
|
5
|
+
// declared function and `let` is available when these handlers and
|
|
6
|
+
// the boot block run.
|
|
7
|
+
//
|
|
8
|
+
// Globals consumed from earlier scripts / app.js:
|
|
9
|
+
// - navigate(), render(), handleRoute(),
|
|
10
|
+
// populateProjectSelect() (app.js)
|
|
11
|
+
// - showCommandLauncher(),
|
|
12
|
+
// nextIsTestValue() (utils/command-launcher.js)
|
|
13
|
+
// - showHelp(), showConfirm(),
|
|
14
|
+
// showError(), closeModal() (utils/modals.js)
|
|
15
|
+
// - showConfigEditor() (utils/config-editor.js)
|
|
16
|
+
// - showStoryDetail() (utils/story-detail-view.js)
|
|
17
|
+
// - showSessionDetail() (utils/sessions-view.js)
|
|
18
|
+
// - selectProject() (app.js)
|
|
19
|
+
// - triggerSync(),
|
|
20
|
+
// startStandbyPolling() (utils/api.js)
|
|
21
|
+
// - subscribeToServerEvents(),
|
|
22
|
+
// smartRefresh() (utils/server-push.js)
|
|
23
|
+
// - selectedProject, currentView,
|
|
24
|
+
// refreshInterval (mutable `let` in app.js —
|
|
25
|
+
// assignment crosses scripts via
|
|
26
|
+
// shared-realm hoisting)
|
|
27
|
+
|
|
28
|
+
// ---- Initialization ----
|
|
29
|
+
|
|
30
|
+
// Nav button clicks
|
|
31
|
+
document.querySelectorAll('.nav-btn').forEach((btn) => {
|
|
32
|
+
btn.addEventListener('click', () => navigate(btn.dataset.view));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Project selector
|
|
36
|
+
document.getElementById('project-select').addEventListener('change', (e) => {
|
|
37
|
+
selectedProject = e.target.value;
|
|
38
|
+
window.location.hash = selectedProject ? `${currentView}/${selectedProject}` : currentView;
|
|
39
|
+
render();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Sync button — re-scan disk for new batches
|
|
43
|
+
document.getElementById('sync-btn').addEventListener('click', async () => {
|
|
44
|
+
const btn = document.getElementById('sync-btn');
|
|
45
|
+
btn.disabled = true;
|
|
46
|
+
btn.textContent = '⏳';
|
|
47
|
+
try {
|
|
48
|
+
await fetch('/api/sync', { method: 'POST' });
|
|
49
|
+
await populateProjectSelect();
|
|
50
|
+
render();
|
|
51
|
+
} catch { /* ignore */ }
|
|
52
|
+
btn.textContent = '🔄';
|
|
53
|
+
btn.disabled = false;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Commands button — opens the launcher modal so the user can run
|
|
57
|
+
// `kj plan`, `kj architect`, `kj clean` (and more later) without
|
|
58
|
+
// dropping to a terminal. Output streams to the existing log panel.
|
|
59
|
+
document.getElementById('commands-btn').addEventListener('click', () => {
|
|
60
|
+
showCommandLauncher();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Help button — KJC-TSK-0372. Quick reference modal for the 5 views.
|
|
64
|
+
// Hover tooltips on each tab cover the 1-line case; this modal is
|
|
65
|
+
// for users who arrive cold and want "ok, what does each one do?".
|
|
66
|
+
document.getElementById('help-btn').addEventListener('click', () => {
|
|
67
|
+
showHelp();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// PR5: Settings button — opens the kj.config.yml editor modal.
|
|
71
|
+
// User picks coder/reviewer providers, model strategy, iteration
|
|
72
|
+
// caps, etc. in plain Spanish. Save writes the yml atomically;
|
|
73
|
+
// next `kj run` picks up the new config (no restart needed).
|
|
74
|
+
document.getElementById('config-btn').addEventListener('click', () => {
|
|
75
|
+
showConfigEditor();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Restart button — respawn the board server in place. The page's
|
|
79
|
+
// EventSource auto-reconnects so the user sees a brief "connecting…"
|
|
80
|
+
// blip and the board comes back live.
|
|
81
|
+
document.getElementById('restart-btn').addEventListener('click', async () => {
|
|
82
|
+
const ok = await showConfirm(
|
|
83
|
+
'Restart the HU Board server now? The page will reconnect automatically.',
|
|
84
|
+
{ title: 'Restart board', okLabel: 'Restart' }
|
|
85
|
+
);
|
|
86
|
+
if (!ok) return;
|
|
87
|
+
try {
|
|
88
|
+
await fetch('/api/board/restart', { method: 'POST' });
|
|
89
|
+
} catch { /* the server is going down; the catch is expected */ }
|
|
90
|
+
// Give the new server a beat to bind, then reload the page.
|
|
91
|
+
setTimeout(() => window.location.reload(), 1200);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
document.addEventListener('click', async (e) => {
|
|
95
|
+
const btn = e.target.closest('.project-card__is-test');
|
|
96
|
+
if (!btn) return;
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
const projectId = btn.dataset.projectId;
|
|
100
|
+
const next = nextIsTestValue(btn.dataset.isTest);
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(projectId)}/is-test`, {
|
|
103
|
+
method: 'PATCH',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({ is_test: next }),
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
108
|
+
// Re-render the project list so the badge reflects the new value.
|
|
109
|
+
await populateProjectSelect();
|
|
110
|
+
render();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
await showError(err.message, { title: 'Failed to update is_test' });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Delete project (cascade) — delegated handler on the dashboard grid
|
|
117
|
+
document.addEventListener('click', async (e) => {
|
|
118
|
+
const btn = e.target.closest('.project-card__delete');
|
|
119
|
+
if (!btn) return;
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
const projectId = btn.dataset.projectId;
|
|
123
|
+
const projectName = btn.dataset.projectName || projectId;
|
|
124
|
+
const ok = await showConfirm(
|
|
125
|
+
`Delete project "${projectName}" and all its stories + sessions?\n\n`
|
|
126
|
+
+ `Also removes ~/.karajan/hu-stories/${projectId}/ from disk.`,
|
|
127
|
+
{ title: 'Delete project', okLabel: 'Delete', destructive: true }
|
|
128
|
+
);
|
|
129
|
+
if (!ok) return;
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE' });
|
|
132
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
133
|
+
await populateProjectSelect();
|
|
134
|
+
render();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
await showError(err.message, { title: 'Failed to delete project' });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Modal close on backdrop click
|
|
141
|
+
document.getElementById('modal-backdrop').addEventListener('click', (e) => {
|
|
142
|
+
if (e.target === e.currentTarget) closeModal();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ESC to close modal
|
|
146
|
+
document.addEventListener('keydown', (e) => {
|
|
147
|
+
if (e.key === 'Escape') closeModal();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Hash routing
|
|
151
|
+
window.addEventListener('hashchange', handleRoute);
|
|
152
|
+
|
|
153
|
+
// Make functions available globally for onclick handlers
|
|
154
|
+
window.showStoryDetail = showStoryDetail;
|
|
155
|
+
window.showSessionDetail = showSessionDetail;
|
|
156
|
+
window.closeModal = closeModal;
|
|
157
|
+
window.selectProject = selectProject;
|
|
158
|
+
|
|
159
|
+
// KJC-TSK-0414 PR4: arranca el polling del banner de standby al cargar la página.
|
|
160
|
+
startStandbyPolling();
|
|
161
|
+
|
|
162
|
+
// Initial load — sync disk data first so new batches are visible
|
|
163
|
+
triggerSync().then(() => {
|
|
164
|
+
populateProjectSelect();
|
|
165
|
+
handleRoute();
|
|
166
|
+
subscribeToServerEvents();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Auto-refresh every 60 seconds as a SAFETY NET only — the board is
|
|
170
|
+
// SSE-driven (push from server) so this should rarely fire. We keep
|
|
171
|
+
// it at low frequency to recover from missed events on hibernation /
|
|
172
|
+
// network blip without flickering the UI every 10s like before.
|
|
173
|
+
// Uses smartRefresh too so it patches the DOM instead of full rerender.
|
|
174
|
+
refreshInterval = setInterval(async () => {
|
|
175
|
+
if (document.getElementById('modal-backdrop').classList.contains('hidden')) {
|
|
176
|
+
await triggerSync();
|
|
177
|
+
await populateProjectSelect();
|
|
178
|
+
await smartRefresh(null);
|
|
179
|
+
}
|
|
180
|
+
}, 60_000);
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// KJC-TSK-0501 step 7/8 — Floating run-log panel + window controls.
|
|
2
|
+
//
|
|
3
|
+
// Classic script (no exports). Loaded by index.html before app.js so
|
|
4
|
+
// the function declarations hoist and inline onclick="openLogViewer(...)"
|
|
5
|
+
// handlers emitted by board-view / dashboard-view resolve transparently.
|
|
6
|
+
// `logPollTimer` and `logViewerState` are top-level `let` bindings in
|
|
7
|
+
// this classic script — same realm as app.js, so the cross-script
|
|
8
|
+
// references keep working unchanged.
|
|
9
|
+
//
|
|
10
|
+
// Globals consumed from earlier scripts:
|
|
11
|
+
// - esc() (app.js)
|
|
12
|
+
// - ansiToHtml() (utils/formatters.js)
|
|
13
|
+
|
|
14
|
+
// `logPollTimer` survives multiple sessions because the panel is a
|
|
15
|
+
// singleton — opening a second log tears the first one down. `panel`
|
|
16
|
+
// is kept on `logViewerState` so the close handler can null it out
|
|
17
|
+
// from inside the tail loop (without it, `tick()` would keep firing
|
|
18
|
+
// HTTP requests against a dead panel).
|
|
19
|
+
let logPollTimer = null;
|
|
20
|
+
let logViewerState = { planId: null, panel: null, offset: 0, isMax: false, isMin: false };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Open (or refocus) the run-log panel for the given plan. Thin shim
|
|
24
|
+
* over `openGenericLogPanel` — the original entry point used for
|
|
25
|
+
* `kj run --plan` runs that the Run-plan button kicks off.
|
|
26
|
+
*/
|
|
27
|
+
function openLogViewer(planId) {
|
|
28
|
+
return openGenericLogPanel({
|
|
29
|
+
id: planId,
|
|
30
|
+
label: 'Run log',
|
|
31
|
+
tailUrl: (offset) => `/api/plans/${encodeURIComponent(planId)}/log?offset=${offset}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generic floating log panel anchored bottom-right. Used by:
|
|
37
|
+
* - openLogViewer(planId) → /api/plans/:planId/log
|
|
38
|
+
* - openCommandLogViewer(cmd, id) → /api/runs/:commandId/log
|
|
39
|
+
*
|
|
40
|
+
* The only thing that changes between callers is the tail URL +
|
|
41
|
+
* the label; the window controls (min/max/close), polling, and ANSI
|
|
42
|
+
* rendering are identical.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} args
|
|
45
|
+
* @param {string} args.id - opaque id (planId or commandId), shown in header
|
|
46
|
+
* @param {string} args.label - "Run log", "kj plan", …
|
|
47
|
+
* @param {(offset:number)=>string} args.tailUrl - returns the URL to GET each tick
|
|
48
|
+
*/
|
|
49
|
+
function openGenericLogPanel({ id, label, tailUrl }) {
|
|
50
|
+
// Tearing down the previous panel cleanly avoids two timers running
|
|
51
|
+
// when the user opens a second log while the first is still tailing.
|
|
52
|
+
if (logPollTimer) { clearTimeout(logPollTimer); logPollTimer = null; }
|
|
53
|
+
if (logViewerState.panel && logViewerState.panel.parentNode) {
|
|
54
|
+
logViewerState.panel.remove();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const panel = document.createElement('section');
|
|
58
|
+
panel.id = 'kj-log-panel';
|
|
59
|
+
panel.dataset.state = 'normal';
|
|
60
|
+
panel.style.cssText = [
|
|
61
|
+
'position:fixed',
|
|
62
|
+
'right:16px',
|
|
63
|
+
'bottom:16px',
|
|
64
|
+
'width:min(720px, calc(100vw - 32px))',
|
|
65
|
+
'height:min(420px, calc(100vh - 80px))',
|
|
66
|
+
'display:flex',
|
|
67
|
+
'flex-direction:column',
|
|
68
|
+
'border:1px solid var(--border)',
|
|
69
|
+
'border-radius:var(--radius-sm)',
|
|
70
|
+
'background:var(--bg-secondary)',
|
|
71
|
+
'box-shadow:0 12px 40px rgba(0,0,0,0.45)',
|
|
72
|
+
'z-index:9999',
|
|
73
|
+
'overflow:hidden',
|
|
74
|
+
].join(';');
|
|
75
|
+
|
|
76
|
+
panel.innerHTML = `
|
|
77
|
+
<header style="display:flex;align-items:center;justify-content:space-between;
|
|
78
|
+
padding:8px 12px;background:var(--bg-primary);
|
|
79
|
+
border-bottom:1px solid var(--border);
|
|
80
|
+
user-select:none;flex-shrink:0">
|
|
81
|
+
<div style="display:flex;align-items:baseline;gap:10px;min-width:0">
|
|
82
|
+
<strong style="color:var(--text)">${esc(label)}</strong>
|
|
83
|
+
<span style="font-family:var(--font-mono, monospace);font-size:0.78rem;
|
|
84
|
+
color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;
|
|
85
|
+
white-space:nowrap">${esc(id)}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
|
|
88
|
+
<span id="log-status" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px">connecting…</span>
|
|
89
|
+
<button id="log-min" type="button" title="Minimize (run keeps going)"
|
|
90
|
+
style="${winBtnStyle('#facc15')}">_</button>
|
|
91
|
+
<button id="log-max" type="button" title="Maximize / Restore"
|
|
92
|
+
style="${winBtnStyle('#34d399')}">▢</button>
|
|
93
|
+
<button id="log-close" type="button" title="Close panel — the run keeps going in the background. Re-open anytime via the “📜 View log” button on the board."
|
|
94
|
+
style="${winBtnStyle('#f87171')}">✕</button>
|
|
95
|
+
</div>
|
|
96
|
+
</header>
|
|
97
|
+
<pre id="log-body"
|
|
98
|
+
style="margin:0;padding:12px 16px;flex:1 1 auto;overflow:auto;
|
|
99
|
+
font-family:var(--font-mono, monospace);font-size:0.8rem;
|
|
100
|
+
line-height:1.45;background:#0b0b0c;color:#e6e6e6;
|
|
101
|
+
white-space:pre-wrap;word-break:break-word"></pre>
|
|
102
|
+
<footer style="padding:6px 12px;border-top:1px solid var(--border);
|
|
103
|
+
font-size:0.7rem;color:var(--text-muted);flex-shrink:0">
|
|
104
|
+
Closing this panel does NOT stop the run. It keeps going in the background;
|
|
105
|
+
reopen with the 📜 View log button on the board.
|
|
106
|
+
</footer>
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
document.body.appendChild(panel);
|
|
110
|
+
logViewerState = { planId: id, panel, offset: 0, isMax: false, isMin: false };
|
|
111
|
+
|
|
112
|
+
const bodyEl = panel.querySelector('#log-body');
|
|
113
|
+
const statusEl = panel.querySelector('#log-status');
|
|
114
|
+
const footerEl = panel.querySelector('footer');
|
|
115
|
+
const minBtn = panel.querySelector('#log-min');
|
|
116
|
+
const maxBtn = panel.querySelector('#log-max');
|
|
117
|
+
const closeBtn = panel.querySelector('#log-close');
|
|
118
|
+
|
|
119
|
+
// ---- Window controls ----
|
|
120
|
+
closeBtn.addEventListener('click', () => {
|
|
121
|
+
if (logPollTimer) { clearTimeout(logPollTimer); logPollTimer = null; }
|
|
122
|
+
panel.remove();
|
|
123
|
+
logViewerState = { planId: null, panel: null, offset: 0, isMax: false, isMin: false };
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
minBtn.addEventListener('click', () => {
|
|
127
|
+
logViewerState.isMin = !logViewerState.isMin;
|
|
128
|
+
if (logViewerState.isMin) {
|
|
129
|
+
// Collapse to header-only pill anchored at bottom-right. Polling
|
|
130
|
+
// continues so the kB counter stays live; click min again to expand.
|
|
131
|
+
panel.style.height = 'auto';
|
|
132
|
+
panel.style.width = 'auto';
|
|
133
|
+
bodyEl.style.display = 'none';
|
|
134
|
+
footerEl.style.display = 'none';
|
|
135
|
+
panel.dataset.state = 'min';
|
|
136
|
+
} else {
|
|
137
|
+
panel.style.height = logViewerState.isMax ? 'calc(100vh - 32px)' : 'min(420px, calc(100vh - 80px))';
|
|
138
|
+
panel.style.width = logViewerState.isMax ? 'calc(100vw - 32px)' : 'min(720px, calc(100vw - 32px))';
|
|
139
|
+
bodyEl.style.display = '';
|
|
140
|
+
footerEl.style.display = '';
|
|
141
|
+
panel.dataset.state = logViewerState.isMax ? 'max' : 'normal';
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
maxBtn.addEventListener('click', () => {
|
|
146
|
+
logViewerState.isMin = false;
|
|
147
|
+
logViewerState.isMax = !logViewerState.isMax;
|
|
148
|
+
bodyEl.style.display = '';
|
|
149
|
+
footerEl.style.display = '';
|
|
150
|
+
if (logViewerState.isMax) {
|
|
151
|
+
panel.style.top = '16px';
|
|
152
|
+
panel.style.left = '16px';
|
|
153
|
+
panel.style.right = '16px';
|
|
154
|
+
panel.style.bottom = '16px';
|
|
155
|
+
panel.style.width = 'auto';
|
|
156
|
+
panel.style.height = 'auto';
|
|
157
|
+
panel.dataset.state = 'max';
|
|
158
|
+
} else {
|
|
159
|
+
panel.style.top = '';
|
|
160
|
+
panel.style.left = '';
|
|
161
|
+
panel.style.right = '16px';
|
|
162
|
+
panel.style.bottom = '16px';
|
|
163
|
+
panel.style.width = 'min(720px, calc(100vw - 32px))';
|
|
164
|
+
panel.style.height = 'min(420px, calc(100vh - 80px))';
|
|
165
|
+
panel.dataset.state = 'normal';
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---- Tail loop ----
|
|
170
|
+
async function tick() {
|
|
171
|
+
if (!logViewerState.panel) return; // user closed
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(tailUrl(logViewerState.offset));
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
statusEl.textContent = `error: HTTP ${res.status}`;
|
|
176
|
+
logPollTimer = setTimeout(tick, 4000);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const body = await res.json();
|
|
180
|
+
if (!body.exists) {
|
|
181
|
+
statusEl.textContent = 'waiting for run to start…';
|
|
182
|
+
} else {
|
|
183
|
+
if (body.content) {
|
|
184
|
+
// Append rendered HTML so ANSI escape codes show as colour
|
|
185
|
+
// instead of literal `[1m…[0m` noise (the user's complaint).
|
|
186
|
+
// Auto-scroll only when the user was already at the bottom,
|
|
187
|
+
// so manual scrollback doesn't fight the tail.
|
|
188
|
+
const atBottom = bodyEl.scrollTop + bodyEl.clientHeight >= bodyEl.scrollHeight - 8;
|
|
189
|
+
bodyEl.insertAdjacentHTML('beforeend', ansiToHtml(body.content));
|
|
190
|
+
if (atBottom) bodyEl.scrollTop = bodyEl.scrollHeight;
|
|
191
|
+
}
|
|
192
|
+
logViewerState.offset = body.size;
|
|
193
|
+
statusEl.textContent = `${(body.size / 1024).toFixed(1)} KB — live`;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
statusEl.textContent = `error: ${err.message}`;
|
|
197
|
+
}
|
|
198
|
+
logPollTimer = setTimeout(tick, 2000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
tick();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Inline style for the three window-control buttons in the log panel. */
|
|
205
|
+
function winBtnStyle(accentColor) {
|
|
206
|
+
return [
|
|
207
|
+
'width:22px',
|
|
208
|
+
'height:22px',
|
|
209
|
+
'padding:0',
|
|
210
|
+
'border:1px solid var(--border)',
|
|
211
|
+
`background:${accentColor}`,
|
|
212
|
+
'color:#0b0b0c',
|
|
213
|
+
'font-weight:700',
|
|
214
|
+
'font-size:0.78rem',
|
|
215
|
+
'line-height:20px',
|
|
216
|
+
'border-radius:50%',
|
|
217
|
+
'cursor:pointer',
|
|
218
|
+
'display:inline-flex',
|
|
219
|
+
'align-items:center',
|
|
220
|
+
'justify-content:center',
|
|
221
|
+
].join(';');
|
|
222
|
+
}
|