claude-controller 0.2.0 → 0.3.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 +2 -2
- package/bin/autoloop.sh +382 -0
- package/bin/ctl +327 -5
- package/bin/native-app.py +5 -2
- package/bin/watchdog.sh +357 -0
- package/cognitive/__init__.py +14 -0
- package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
- package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
- package/cognitive/dispatcher.py +192 -0
- package/cognitive/evaluator.py +289 -0
- package/cognitive/goal_engine.py +232 -0
- package/cognitive/learning.py +189 -0
- package/cognitive/orchestrator.py +303 -0
- package/cognitive/planner.py +207 -0
- package/cognitive/prompts/analyst.md +31 -0
- package/cognitive/prompts/coder.md +22 -0
- package/cognitive/prompts/reviewer.md +33 -0
- package/cognitive/prompts/tester.md +21 -0
- package/cognitive/prompts/writer.md +25 -0
- package/config.sh +6 -1
- package/dag/__init__.py +5 -0
- package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
- package/dag/__pycache__/graph.cpython-314.pyc +0 -0
- package/dag/graph.py +222 -0
- package/lib/jobs.sh +12 -1
- package/package.json +5 -1
- package/postinstall.sh +1 -1
- package/service/controller.sh +43 -11
- package/web/audit.py +122 -0
- package/web/checkpoint.py +80 -0
- package/web/config.py +2 -5
- package/web/handler.py +464 -26
- package/web/handler_fs.py +15 -14
- package/web/handler_goals.py +203 -0
- package/web/handler_jobs.py +165 -42
- package/web/handler_memory.py +203 -0
- package/web/jobs.py +576 -12
- package/web/personas.py +419 -0
- package/web/pipeline.py +682 -50
- package/web/presets.py +506 -0
- package/web/projects.py +58 -4
- package/web/static/api.js +90 -3
- package/web/static/app.js +8 -0
- package/web/static/base.css +51 -12
- package/web/static/context.js +14 -4
- package/web/static/form.css +3 -2
- package/web/static/goals.css +363 -0
- package/web/static/goals.js +300 -0
- package/web/static/i18n.js +288 -0
- package/web/static/index.html +142 -6
- package/web/static/jobs.css +951 -4
- package/web/static/jobs.js +890 -54
- package/web/static/memoryview.js +117 -0
- package/web/static/personas.js +228 -0
- package/web/static/pipeline.css +308 -1
- package/web/static/pipelines.js +249 -14
- package/web/static/presets.js +244 -0
- package/web/static/send.js +26 -4
- package/web/static/settings-style.css +34 -3
- package/web/static/settings.js +37 -1
- package/web/static/stream.js +242 -19
- package/web/static/utils.js +54 -2
- package/web/webhook.py +210 -0
package/web/static/pipelines.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
let _pipelines = [];
|
|
7
7
|
let _pipelinePollTimer = null;
|
|
8
8
|
let _pipelineCountdownTimer = null;
|
|
9
|
+
const _POLL_FAST = 2000; // job 실행 중
|
|
10
|
+
const _POLL_NORMAL = 5000; // 대기 중
|
|
9
11
|
|
|
10
12
|
async function fetchPipelines() {
|
|
11
13
|
try {
|
|
@@ -13,17 +15,27 @@ async function fetchPipelines() {
|
|
|
13
15
|
renderPipelines();
|
|
14
16
|
|
|
15
17
|
const hasActive = _pipelines.some(p => p.status === 'active');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const hasRunning = _pipelines.some(p => p.status === 'active' && p.job_id);
|
|
19
|
+
const desiredInterval = hasRunning ? _POLL_FAST : _POLL_NORMAL;
|
|
20
|
+
|
|
21
|
+
if (hasActive) {
|
|
22
|
+
// 간격이 바뀌었으면 타이머 재설정
|
|
23
|
+
if (_pipelinePollTimer && _pipelinePollTimer._interval !== desiredInterval) {
|
|
24
|
+
clearInterval(_pipelinePollTimer);
|
|
25
|
+
_pipelinePollTimer = null;
|
|
26
|
+
}
|
|
27
|
+
if (!_pipelinePollTimer) {
|
|
28
|
+
_pipelinePollTimer = setInterval(_pipelinePollTick, desiredInterval);
|
|
29
|
+
_pipelinePollTimer._interval = desiredInterval;
|
|
30
|
+
}
|
|
31
|
+
} else if (_pipelinePollTimer) {
|
|
19
32
|
clearInterval(_pipelinePollTimer);
|
|
20
33
|
_pipelinePollTimer = null;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
if (hasTimer && !_pipelineCountdownTimer) {
|
|
36
|
+
if (hasActive && !_pipelineCountdownTimer) {
|
|
25
37
|
_pipelineCountdownTimer = setInterval(_updateCountdowns, 1000);
|
|
26
|
-
} else if (!
|
|
38
|
+
} else if (!hasActive && _pipelineCountdownTimer) {
|
|
27
39
|
clearInterval(_pipelineCountdownTimer);
|
|
28
40
|
_pipelineCountdownTimer = null;
|
|
29
41
|
}
|
|
@@ -31,12 +43,16 @@ async function fetchPipelines() {
|
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
async function _pipelinePollTick() {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
46
|
+
const hasActive = _pipelines.some(p => p.status === 'active');
|
|
47
|
+
if (hasActive) {
|
|
36
48
|
try {
|
|
37
|
-
await apiFetch('/api/pipelines/tick-all', {
|
|
49
|
+
const results = await apiFetch('/api/pipelines/tick-all', {
|
|
38
50
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
|
|
39
51
|
});
|
|
52
|
+
// 완료/에러/디스패치 감지 시 jobs 목록도 즉시 갱신
|
|
53
|
+
if (Array.isArray(results) && results.some(r => r.result && (r.result.action === 'completed' || r.result.action === 'error' || r.result.action === 'dispatched'))) {
|
|
54
|
+
fetchJobs();
|
|
55
|
+
}
|
|
40
56
|
} catch { /* silent */ }
|
|
41
57
|
}
|
|
42
58
|
fetchPipelines();
|
|
@@ -56,14 +72,23 @@ function _updateCountdowns() {
|
|
|
56
72
|
|
|
57
73
|
if (!p.next_run) {
|
|
58
74
|
el.textContent = _formatTimer(0);
|
|
75
|
+
// next_run 없고 job도 없으면 즉시 dispatch (프리셋 생성 직후 등)
|
|
76
|
+
if (!p._dispatching && !p.job_id) {
|
|
77
|
+
p._dispatching = true;
|
|
78
|
+
apiFetch(`/api/pipelines/${encodeURIComponent(p.id)}/run`, {
|
|
79
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
|
|
80
|
+
}).then(() => { fetchPipelines(); fetchJobs(); setTimeout(fetchJobs, 1000); })
|
|
81
|
+
.catch(() => {})
|
|
82
|
+
.finally(() => { p._dispatching = false; });
|
|
83
|
+
}
|
|
59
84
|
continue;
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
const remaining = Math.max(0, Math.floor((new Date(p.next_run).getTime() - Date.now()) / 1000));
|
|
63
88
|
el.textContent = _formatTimer(remaining);
|
|
64
89
|
|
|
65
|
-
// 타이머 0 →
|
|
66
|
-
if (remaining <= 0 && !p._dispatching) {
|
|
90
|
+
// 타이머 0 → 이미 job_id가 있으면 skip (이중 발사 방지)
|
|
91
|
+
if (remaining <= 0 && !p._dispatching && !p.job_id) {
|
|
67
92
|
p._dispatching = true;
|
|
68
93
|
// 로컬에서 즉시 next_run 갱신 → 타이머 끊김 방지
|
|
69
94
|
if (p.interval_sec) {
|
|
@@ -104,18 +129,40 @@ function renderPipelines() {
|
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
const cmdPreview = (p.command || '').substring(0, 80) + ((p.command || '').length > 80 ? '...' : '');
|
|
107
|
-
const
|
|
132
|
+
const projectName = p.project_path ? p.project_path.split('/').filter(Boolean).pop() : '';
|
|
133
|
+
const projectPath = p.project_path || '';
|
|
134
|
+
|
|
135
|
+
// 적응형 인터벌 표시
|
|
136
|
+
const baseInterval = p.interval_sec;
|
|
137
|
+
const effectiveInterval = p.effective_interval_sec;
|
|
138
|
+
const isAdapted = baseInterval && effectiveInterval && baseInterval !== effectiveInterval;
|
|
139
|
+
let intervalLabel = '';
|
|
140
|
+
if (p.interval) {
|
|
141
|
+
if (isAdapted) {
|
|
142
|
+
const effMin = Math.round(effectiveInterval / 60);
|
|
143
|
+
const pct = Math.round((effectiveInterval - baseInterval) / baseInterval * 100);
|
|
144
|
+
intervalLabel = `<span style="font-size:0.65rem;padding:1px 5px;background:rgba(168,85,247,0.1);color:#a855f7;border-radius:3px;" title="기본: ${escapeHtml(p.interval)}, 적응: ${effMin}분 (${pct > 0 ? '+' : ''}${pct}%)">${effMin}분 적응</span>`;
|
|
145
|
+
} else {
|
|
146
|
+
intervalLabel = `<span style="font-size:0.65rem;padding:1px 5px;background:rgba(59,130,246,0.1);color:var(--primary);border-radius:3px;">${escapeHtml(p.interval)} 반복</span>`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 체이닝 표시
|
|
151
|
+
const chainLabel = p.on_complete ? `<span style="font-size:0.65rem;padding:1px 5px;background:rgba(34,197,94,0.1);color:#22c55e;border-radius:3px;" title="완료 시 트리거">→ chain</span>` : '';
|
|
152
|
+
|
|
153
|
+
const runningBadge = isRunning ? `<span class="pipe-running-badge"><span class="pipe-running-dot"></span>실행 중</span>` : '';
|
|
108
154
|
const runCount = p.run_count ? `<span style="font-size:0.65rem;color:var(--text-muted);">${p.run_count}회</span>` : '';
|
|
109
155
|
|
|
110
156
|
const toggleBtn = isOn
|
|
111
157
|
? `<button class="btn btn-sm" onclick="stopPipeline('${p.id}')">OFF</button>`
|
|
112
158
|
: `<button class="btn btn-sm btn-primary" onclick="runPipeline('${p.id}')">ON</button>`;
|
|
113
159
|
|
|
114
|
-
return `<div class="pipeline-card" data-pipe-id="${p.id}">
|
|
160
|
+
return `<div class="pipeline-card${isRunning ? ' is-running' : ''}" data-pipe-id="${p.id}">
|
|
115
161
|
<div class="pipeline-card-header">
|
|
116
162
|
<div class="pipeline-card-title">${escapeHtml(p.name || p.id)}</div>
|
|
117
|
-
<div style="display:flex;gap:4px;align-items:center;">${intervalLabel} ${timerHtml}</div>
|
|
163
|
+
<div style="display:flex;gap:4px;align-items:center;">${runningBadge} ${intervalLabel} ${chainLabel} ${timerHtml}</div>
|
|
118
164
|
</div>
|
|
165
|
+
${projectName ? `<div style="font-size:0.65rem;color:var(--text-muted);margin:2px 0 0 0;display:flex;align-items:center;gap:3px;" title="${escapeHtml(projectPath)}"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>${escapeHtml(projectName)}</div>` : ''}
|
|
119
166
|
<div class="pipeline-card-goal" style="font-size:0.75rem;color:var(--text-secondary);margin:4px 0;font-family:var(--font-mono,monospace);">${escapeHtml(cmdPreview)}</div>
|
|
120
167
|
${p.last_error ? `<div style="font-size:0.7rem;color:var(--danger);margin:4px 0;padding:4px 8px;background:rgba(239,68,68,0.1);border-radius:4px;">${escapeHtml(p.last_error)}</div>` : ''}
|
|
121
168
|
<div style="display:flex;gap:8px;font-size:0.7rem;color:var(--text-muted);align-items:center;">
|
|
@@ -125,6 +172,7 @@ function renderPipelines() {
|
|
|
125
172
|
<div class="pipeline-card-actions" style="margin-top:8px;display:flex;gap:6px;">
|
|
126
173
|
${toggleBtn}
|
|
127
174
|
<button class="btn btn-sm" onclick="editPipeline('${p.id}')">수정</button>
|
|
175
|
+
${p.run_count ? `<button class="btn btn-sm" onclick="showPipelineHistory('${p.id}')">이력</button>` : ''}
|
|
128
176
|
<button class="btn btn-sm btn-danger" onclick="deletePipeline('${p.id}')">삭제</button>
|
|
129
177
|
</div>
|
|
130
178
|
</div>`;
|
|
@@ -250,3 +298,190 @@ async function deletePipeline(pipeId) {
|
|
|
250
298
|
showToast(err.message || '삭제 실패', 'error');
|
|
251
299
|
}
|
|
252
300
|
}
|
|
301
|
+
|
|
302
|
+
/* ── 진화 요약 패널 ── */
|
|
303
|
+
async function fetchEvolutionSummary() {
|
|
304
|
+
const el = document.getElementById('evolutionSummary');
|
|
305
|
+
if (!el) return;
|
|
306
|
+
try {
|
|
307
|
+
const data = await apiFetch('/api/pipelines/evolution');
|
|
308
|
+
const cls = data.classifications || {};
|
|
309
|
+
const total = (cls.has_change || 0) + (cls.no_change || 0) + (cls.unknown || 0);
|
|
310
|
+
const adaptations = (data.interval_adaptations || []);
|
|
311
|
+
|
|
312
|
+
let adaptHtml = '';
|
|
313
|
+
if (adaptations.length > 0) {
|
|
314
|
+
adaptHtml = adaptations.map(a =>
|
|
315
|
+
`<span style="font-size:0.65rem;color:#a855f7;">${escapeHtml(a.name)}: ${a.change_pct > 0 ? '+' : ''}${a.change_pct}%</span>`
|
|
316
|
+
).join(' ');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
el.innerHTML = `<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:8px 12px;background:var(--bg-secondary);border-radius:8px;font-size:0.7rem;color:var(--text-muted);">
|
|
320
|
+
<span>총 ${data.total_runs}회 실행</span>
|
|
321
|
+
<span>효율 ${data.efficiency_pct}%</span>
|
|
322
|
+
${total > 0 ? `<span style="color:var(--text-secondary);">변경:${cls.has_change||0} / 무변경:${cls.no_change||0}</span>` : ''}
|
|
323
|
+
${adaptHtml}
|
|
324
|
+
</div>`;
|
|
325
|
+
} catch { el.innerHTML = ''; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ── 새 자동화 생성 모달 ── */
|
|
329
|
+
function openCreatePipeline() {
|
|
330
|
+
const cwd = document.getElementById('cwdInput')?.value?.trim() || '';
|
|
331
|
+
const formId = 'pipeCreate_' + Date.now();
|
|
332
|
+
const overlay = document.createElement('div');
|
|
333
|
+
overlay.className = 'settings-overlay';
|
|
334
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
335
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
336
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:520px;margin:0;">
|
|
337
|
+
<div class="settings-header">
|
|
338
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
339
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
340
|
+
<span>새 자동화</span>
|
|
341
|
+
</div>
|
|
342
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
343
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
<div class="settings-body">
|
|
347
|
+
<div style="margin-bottom:12px;">
|
|
348
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">이름</label>
|
|
349
|
+
<input id="${formId}_name" type="text" placeholder="예: code-quality"
|
|
350
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
|
|
351
|
+
</div>
|
|
352
|
+
<div style="margin-bottom:12px;">
|
|
353
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">프로젝트 경로</label>
|
|
354
|
+
<input id="${formId}_path" type="text" value="${escapeHtml(cwd)}"
|
|
355
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;font-family:var(--font-mono,monospace);box-sizing:border-box;">
|
|
356
|
+
</div>
|
|
357
|
+
<div style="margin-bottom:12px;">
|
|
358
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">명령어 (프롬프트)</label>
|
|
359
|
+
<textarea id="${formId}_cmd" rows="4" placeholder="Claude에게 시킬 작업을 입력하세요"
|
|
360
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;font-family:var(--font-mono,monospace);resize:vertical;box-sizing:border-box;"></textarea>
|
|
361
|
+
</div>
|
|
362
|
+
<div style="display:flex;gap:12px;margin-bottom:12px;">
|
|
363
|
+
<div style="flex:1;">
|
|
364
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">반복 간격 <span style="font-weight:400;color:var(--text-muted);">(비우면 1회)</span></label>
|
|
365
|
+
<input id="${formId}_interval" type="text" placeholder="예: 5m, 1h"
|
|
366
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
|
|
367
|
+
</div>
|
|
368
|
+
<div style="flex:1;">
|
|
369
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">체이닝 <span style="font-weight:400;color:var(--text-muted);">(완료 시 트리거)</span></label>
|
|
370
|
+
<select id="${formId}_chain"
|
|
371
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
|
|
372
|
+
<option value="">없음</option>
|
|
373
|
+
${_pipelines.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name || p.id)}</option>`).join('')}
|
|
374
|
+
</select>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="settings-footer" style="display:flex;gap:6px;">
|
|
379
|
+
<button class="btn btn-sm btn-primary" onclick="_submitCreatePipeline('${formId}',this)">생성 및 실행</button>
|
|
380
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>`;
|
|
383
|
+
document.body.appendChild(overlay);
|
|
384
|
+
// 포커스
|
|
385
|
+
setTimeout(() => document.getElementById(formId + '_name')?.focus(), 100);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function _submitCreatePipeline(formId, btn) {
|
|
389
|
+
const name = document.getElementById(formId + '_name').value.trim();
|
|
390
|
+
const project_path = document.getElementById(formId + '_path').value.trim();
|
|
391
|
+
const command = document.getElementById(formId + '_cmd').value.trim();
|
|
392
|
+
const interval = document.getElementById(formId + '_interval').value.trim();
|
|
393
|
+
const on_complete = document.getElementById(formId + '_chain').value;
|
|
394
|
+
|
|
395
|
+
if (!project_path) { showToast('프로젝트 경로를 입력하세요', 'error'); return; }
|
|
396
|
+
if (!command) { showToast('명령어를 입력하세요', 'error'); return; }
|
|
397
|
+
|
|
398
|
+
btn.disabled = true;
|
|
399
|
+
btn.textContent = '생성 중...';
|
|
400
|
+
try {
|
|
401
|
+
await apiFetch('/api/pipelines', {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
headers: { 'Content-Type': 'application/json' },
|
|
404
|
+
body: JSON.stringify({ name, project_path, command, interval, on_complete }),
|
|
405
|
+
});
|
|
406
|
+
showToast('자동화 생성 완료');
|
|
407
|
+
btn.closest('.settings-overlay').remove();
|
|
408
|
+
fetchPipelines();
|
|
409
|
+
fetchJobs();
|
|
410
|
+
} catch (err) {
|
|
411
|
+
showToast(err.message || '생성 실패', 'error');
|
|
412
|
+
btn.disabled = false;
|
|
413
|
+
btn.textContent = '생성 및 실행';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* ── 실행 이력 모달 ── */
|
|
418
|
+
async function showPipelineHistory(pipeId) {
|
|
419
|
+
try {
|
|
420
|
+
const data = await apiFetch(`/api/pipelines/${encodeURIComponent(pipeId)}/history`);
|
|
421
|
+
const overlay = document.createElement('div');
|
|
422
|
+
overlay.className = 'settings-overlay';
|
|
423
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
424
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
425
|
+
|
|
426
|
+
const entries = data.entries || [];
|
|
427
|
+
let tableHtml = '';
|
|
428
|
+
if (entries.length === 0) {
|
|
429
|
+
tableHtml = '<div style="padding:20px;text-align:center;color:var(--text-muted);font-size:0.8rem;">실행 이력이 없습니다.</div>';
|
|
430
|
+
} else {
|
|
431
|
+
const rows = entries.map((h, i) => {
|
|
432
|
+
const cls = h.classification || 'unknown';
|
|
433
|
+
const clsBadge = cls === 'has_change'
|
|
434
|
+
? '<span class="pipe-hist-badge pipe-hist-change">변경</span>'
|
|
435
|
+
: cls === 'no_change'
|
|
436
|
+
? '<span class="pipe-hist-badge pipe-hist-nochange">무변경</span>'
|
|
437
|
+
: '<span class="pipe-hist-badge pipe-hist-unknown">?</span>';
|
|
438
|
+
const dur = h.duration_ms ? `${(h.duration_ms / 1000).toFixed(1)}s` : '-';
|
|
439
|
+
const time = h.completed_at || '';
|
|
440
|
+
const resultPreview = escapeHtml((h.result || '').substring(0, 120));
|
|
441
|
+
return `<tr>
|
|
442
|
+
<td style="white-space:nowrap;">${time}</td>
|
|
443
|
+
<td>${clsBadge}</td>
|
|
444
|
+
<td>${dur}</td>
|
|
445
|
+
<td class="pipe-hist-result" title="${escapeHtml(h.result || '')}">${resultPreview}${(h.result || '').length > 120 ? '...' : ''}</td>
|
|
446
|
+
</tr>`;
|
|
447
|
+
}).join('');
|
|
448
|
+
tableHtml = `<div class="pipe-hist-table-wrap"><table class="pipe-hist-table">
|
|
449
|
+
<thead><tr><th>시간</th><th>분류</th><th>소요</th><th>결과 요약</th></tr></thead>
|
|
450
|
+
<tbody>${rows}</tbody>
|
|
451
|
+
</table></div>`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const summaryHtml = `<div style="display:flex;gap:12px;font-size:0.72rem;color:var(--text-muted);margin-bottom:12px;">
|
|
455
|
+
<span>총 ${data.run_count}회 실행</span>
|
|
456
|
+
</div>`;
|
|
457
|
+
|
|
458
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:700px;margin:0;">
|
|
459
|
+
<div class="settings-header">
|
|
460
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
461
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
462
|
+
<span>${escapeHtml(data.name || pipeId)} — 실행 이력</span>
|
|
463
|
+
</div>
|
|
464
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
465
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
466
|
+
</button>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="settings-body" style="padding:12px 16px;">
|
|
469
|
+
${summaryHtml}
|
|
470
|
+
${tableHtml}
|
|
471
|
+
</div>
|
|
472
|
+
<div class="settings-footer">
|
|
473
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">닫기</button>
|
|
474
|
+
</div>
|
|
475
|
+
</div>`;
|
|
476
|
+
document.body.appendChild(overlay);
|
|
477
|
+
} catch (err) {
|
|
478
|
+
showToast(err.message || '이력 조회 실패', 'error');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// fetchPipelines 후 진화 요약도 갱신
|
|
483
|
+
const _origFetchPipelines = fetchPipelines;
|
|
484
|
+
fetchPipelines = async function() {
|
|
485
|
+
await _origFetchPipelines();
|
|
486
|
+
fetchEvolutionSummary();
|
|
487
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/* ===================================================
|
|
2
|
+
Presets -- 자동화 프리셋 브라우징/적용 UI
|
|
3
|
+
=================================================== */
|
|
4
|
+
|
|
5
|
+
let _presets = [];
|
|
6
|
+
let _presetsPanelOpen = false;
|
|
7
|
+
|
|
8
|
+
const _PRESET_ICONS = {
|
|
9
|
+
rocket: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>',
|
|
10
|
+
search: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
11
|
+
book: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
|
12
|
+
shield: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
|
|
13
|
+
layers: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
|
|
14
|
+
bookmark: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function fetchPresets() {
|
|
18
|
+
try {
|
|
19
|
+
_presets = await apiFetch('/api/presets');
|
|
20
|
+
renderPresetCards();
|
|
21
|
+
} catch { /* silent */ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderPresetCards() {
|
|
25
|
+
const container = document.getElementById('presetGrid');
|
|
26
|
+
if (!container) return;
|
|
27
|
+
|
|
28
|
+
if (_presets.length === 0) {
|
|
29
|
+
container.innerHTML = '<div class="empty-state" style="padding:20px;text-align:center;color:var(--text-muted);font-size:0.8rem;">프리셋이 없습니다.</div>';
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
container.innerHTML = _presets.map(p => {
|
|
34
|
+
const icon = _PRESET_ICONS[p.icon] || _PRESET_ICONS.layers;
|
|
35
|
+
const pipeNames = (p.pipeline_names || []).map(n => escapeHtml(n)).join(' → ');
|
|
36
|
+
const builtinBadge = p.builtin
|
|
37
|
+
? '<span class="preset-badge preset-badge-builtin">내장</span>'
|
|
38
|
+
: '<span class="preset-badge preset-badge-custom">커스텀</span>';
|
|
39
|
+
|
|
40
|
+
return `<div class="preset-card" data-preset-id="${escapeHtml(p.id)}">
|
|
41
|
+
<div class="preset-card-icon">${icon}</div>
|
|
42
|
+
<div class="preset-card-body">
|
|
43
|
+
<div class="preset-card-header">
|
|
44
|
+
<span class="preset-card-name">${escapeHtml(p.name)}</span>
|
|
45
|
+
${builtinBadge}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="preset-card-desc">${escapeHtml(p.description)}</div>
|
|
48
|
+
<div class="preset-card-pipes">${pipeNames} <span class="preset-pipe-count">(${p.pipeline_count}개)</span></div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="preset-card-actions">
|
|
51
|
+
<button class="btn btn-sm btn-primary" onclick="openApplyPreset('${escapeHtml(p.id)}')">적용</button>
|
|
52
|
+
${!p.builtin ? `<button class="btn btn-sm btn-danger" onclick="deletePreset('${escapeHtml(p.id)}')">삭제</button>` : ''}
|
|
53
|
+
</div>
|
|
54
|
+
</div>`;
|
|
55
|
+
}).join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function openApplyPreset(presetId) {
|
|
59
|
+
const preset = _presets.find(p => p.id === presetId);
|
|
60
|
+
if (!preset) return;
|
|
61
|
+
|
|
62
|
+
const overlayId = 'presetApply_' + Date.now();
|
|
63
|
+
const overlay = document.createElement('div');
|
|
64
|
+
overlay.className = 'settings-overlay';
|
|
65
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
66
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
67
|
+
|
|
68
|
+
const icon = _PRESET_ICONS[preset.icon] || _PRESET_ICONS.layers;
|
|
69
|
+
|
|
70
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:480px;margin:0;">
|
|
71
|
+
<div class="settings-header">
|
|
72
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
73
|
+
${icon}
|
|
74
|
+
<span>프리셋 적용: ${escapeHtml(preset.name)}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
77
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="settings-body">
|
|
81
|
+
<div style="margin-bottom:12px;font-size:0.78rem;color:var(--text-secondary);">
|
|
82
|
+
${escapeHtml(preset.description)}
|
|
83
|
+
</div>
|
|
84
|
+
<div style="margin-bottom:12px;font-size:0.72rem;color:var(--text-muted);display:flex;gap:6px;flex-wrap:wrap;">
|
|
85
|
+
${(preset.pipeline_names || []).map(n => `<span class="preset-pipe-tag">${escapeHtml(n)}</span>`).join('')}
|
|
86
|
+
</div>
|
|
87
|
+
<div style="margin-bottom:12px;">
|
|
88
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">프로젝트 경로</label>
|
|
89
|
+
<div style="display:flex;gap:6px;">
|
|
90
|
+
<input id="${overlayId}_path" type="text" placeholder="/path/to/project"
|
|
91
|
+
value="${escapeHtml(document.getElementById('cwdInput')?.value || '')}"
|
|
92
|
+
style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;font-family:var(--font-mono,monospace);">
|
|
93
|
+
<button class="btn btn-sm" onclick="_browseForPreset('${overlayId}_path')" title="디렉토리 탐색">
|
|
94
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="settings-footer" style="display:flex;gap:6px;">
|
|
100
|
+
<button class="btn btn-sm btn-primary" onclick="_doApplyPreset('${escapeHtml(presetId)}','${overlayId}',this)">적용하기</button>
|
|
101
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>`;
|
|
104
|
+
document.body.appendChild(overlay);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _browseForPreset(inputId) {
|
|
108
|
+
// 기존 디렉토리 브라우저를 재활용: 선택 후 해당 input에 값 설정
|
|
109
|
+
const input = document.getElementById(inputId);
|
|
110
|
+
if (!input) return;
|
|
111
|
+
// 임시로 cwdInput에 연결하여 기존 dir browser 활용
|
|
112
|
+
const origCwd = document.getElementById('cwdInput')?.value || '';
|
|
113
|
+
toggleDirBrowser();
|
|
114
|
+
// dirBrowser 선택 시 cwdInput에 값이 설정되므로 polling으로 감지
|
|
115
|
+
const poll = setInterval(() => {
|
|
116
|
+
const newVal = document.getElementById('cwdInput')?.value || '';
|
|
117
|
+
if (newVal && newVal !== origCwd) {
|
|
118
|
+
input.value = newVal;
|
|
119
|
+
clearInterval(poll);
|
|
120
|
+
}
|
|
121
|
+
}, 300);
|
|
122
|
+
setTimeout(() => clearInterval(poll), 30000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function _doApplyPreset(presetId, overlayId, btn) {
|
|
126
|
+
const path = document.getElementById(overlayId + '_path').value.trim();
|
|
127
|
+
if (!path) { showToast('프로젝트 경로를 입력하세요', 'error'); return; }
|
|
128
|
+
|
|
129
|
+
btn.disabled = true;
|
|
130
|
+
btn.textContent = '적용 중...';
|
|
131
|
+
try {
|
|
132
|
+
const result = await apiFetch('/api/presets/apply', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({ preset_id: presetId, project_path: path }),
|
|
136
|
+
});
|
|
137
|
+
showToast(`프리셋 "${result.preset_name}" 적용 완료 (${result.pipelines_created}개 파이프라인 생성)`);
|
|
138
|
+
btn.closest('.settings-overlay').remove();
|
|
139
|
+
// 생성된 파이프라인 중 첫 번째를 즉시 실행 (나머지는 체이닝 또는 타이머로 자동 시작)
|
|
140
|
+
if (result.pipelines && result.pipelines.length > 0) {
|
|
141
|
+
runPipeline(result.pipelines[0].id);
|
|
142
|
+
}
|
|
143
|
+
fetchPipelines();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
showToast(`적용 실패: ${err.message}`, 'error');
|
|
146
|
+
btn.disabled = false;
|
|
147
|
+
btn.textContent = '적용하기';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function deletePreset(presetId) {
|
|
152
|
+
if (!confirm('이 프리셋을 삭제하시겠습니까?')) return;
|
|
153
|
+
try {
|
|
154
|
+
await apiFetch(`/api/presets/${encodeURIComponent(presetId)}`, { method: 'DELETE' });
|
|
155
|
+
showToast('프리셋 삭제됨');
|
|
156
|
+
fetchPresets();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
showToast(err.message || '삭제 실패', 'error');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* -- 현재 파이프라인을 프리셋으로 저장 -- */
|
|
163
|
+
|
|
164
|
+
function openSavePresetDialog() {
|
|
165
|
+
if (_pipelines.length === 0) {
|
|
166
|
+
showToast('저장할 파이프라인이 없습니다', 'error');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const dlgId = 'presetSave_' + Date.now();
|
|
170
|
+
const overlay = document.createElement('div');
|
|
171
|
+
overlay.className = 'settings-overlay';
|
|
172
|
+
overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
|
|
173
|
+
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
|
|
174
|
+
|
|
175
|
+
const pipeChecks = _pipelines.map(p =>
|
|
176
|
+
`<label style="display:flex;gap:6px;align-items:center;font-size:0.78rem;color:var(--text-primary);">
|
|
177
|
+
<input type="checkbox" value="${escapeHtml(p.id)}" checked style="accent-color:var(--primary);">
|
|
178
|
+
${escapeHtml(p.name || p.id)}
|
|
179
|
+
<span style="font-size:0.65rem;color:var(--text-muted);">${escapeHtml(p.interval || '1회')}</span>
|
|
180
|
+
</label>`
|
|
181
|
+
).join('');
|
|
182
|
+
|
|
183
|
+
overlay.innerHTML = `<div class="settings-panel" style="max-width:440px;margin:0;">
|
|
184
|
+
<div class="settings-header">
|
|
185
|
+
<div class="settings-title" style="display:flex;align-items:center;gap:8px;">
|
|
186
|
+
${_PRESET_ICONS.bookmark}
|
|
187
|
+
<span>프리셋으로 저장</span>
|
|
188
|
+
</div>
|
|
189
|
+
<button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
|
|
190
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="settings-body">
|
|
194
|
+
<div style="margin-bottom:12px;">
|
|
195
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">프리셋 이름</label>
|
|
196
|
+
<input id="${dlgId}_name" type="text" placeholder="나의 자동화 프리셋"
|
|
197
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
|
|
198
|
+
</div>
|
|
199
|
+
<div style="margin-bottom:12px;">
|
|
200
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">설명</label>
|
|
201
|
+
<input id="${dlgId}_desc" type="text" placeholder="이 프리셋의 용도를 설명하세요"
|
|
202
|
+
style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
|
|
203
|
+
</div>
|
|
204
|
+
<div style="margin-bottom:8px;">
|
|
205
|
+
<label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:6px;">포함할 파이프라인</label>
|
|
206
|
+
<div id="${dlgId}_pipes" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;">
|
|
207
|
+
${pipeChecks}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="settings-footer" style="display:flex;gap:6px;">
|
|
212
|
+
<button class="btn btn-sm btn-primary" onclick="_doSavePreset('${dlgId}',this)">저장</button>
|
|
213
|
+
<button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>`;
|
|
216
|
+
document.body.appendChild(overlay);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function _doSavePreset(dlgId, btn) {
|
|
220
|
+
const name = document.getElementById(dlgId + '_name').value.trim();
|
|
221
|
+
if (!name) { showToast('프리셋 이름을 입력하세요', 'error'); return; }
|
|
222
|
+
|
|
223
|
+
const desc = document.getElementById(dlgId + '_desc').value.trim();
|
|
224
|
+
const checks = document.querySelectorAll(`#${dlgId}_pipes input[type="checkbox"]:checked`);
|
|
225
|
+
const pipeIds = Array.from(checks).map(c => c.value);
|
|
226
|
+
if (pipeIds.length === 0) { showToast('최소 1개 파이프라인을 선택하세요', 'error'); return; }
|
|
227
|
+
|
|
228
|
+
btn.disabled = true;
|
|
229
|
+
btn.textContent = '저장 중...';
|
|
230
|
+
try {
|
|
231
|
+
const result = await apiFetch('/api/presets/save', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify({ name, description: desc, pipeline_ids: pipeIds }),
|
|
235
|
+
});
|
|
236
|
+
showToast(`프리셋 "${result.name}" 저장됨 (${result.pipeline_count}개 파이프라인)`);
|
|
237
|
+
btn.closest('.settings-overlay').remove();
|
|
238
|
+
fetchPresets();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
showToast(`저장 실패: ${err.message}`, 'error');
|
|
241
|
+
btn.disabled = false;
|
|
242
|
+
btn.textContent = '저장';
|
|
243
|
+
}
|
|
244
|
+
}
|
package/web/static/send.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
let _sendLock = false;
|
|
6
6
|
let _automationMode = false;
|
|
7
|
+
let _depsMode = false;
|
|
7
8
|
|
|
8
9
|
function toggleAutomation() {
|
|
9
10
|
_automationMode = !_automationMode;
|
|
@@ -22,12 +23,24 @@ function toggleAutomation() {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function toggleDeps() {
|
|
27
|
+
// DAG UI는 숨김 처리됨 — AI가 API depends_on으로 직접 사용
|
|
28
|
+
_depsMode = !_depsMode;
|
|
29
|
+
const row = document.getElementById('depsRow');
|
|
30
|
+
if (row) row.style.display = _depsMode ? 'flex' : 'none';
|
|
31
|
+
if (!_depsMode) {
|
|
32
|
+
const inp = document.getElementById('depsInput');
|
|
33
|
+
if (inp) inp.value = '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
function clearPromptForm() {
|
|
26
38
|
document.getElementById('promptInput').value = '';
|
|
27
39
|
clearAttachments();
|
|
28
40
|
updatePromptMirror();
|
|
29
41
|
clearDirSelection();
|
|
30
42
|
if (_automationMode) toggleAutomation();
|
|
43
|
+
if (_depsMode) toggleDeps();
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
async function sendTask(e) {
|
|
@@ -54,8 +67,7 @@ async function sendTask(e) {
|
|
|
54
67
|
body: JSON.stringify({ project_path: cwd, command: prompt, interval }),
|
|
55
68
|
});
|
|
56
69
|
showToast(`자동화 등록: ${pipe.name || pipe.id}`);
|
|
57
|
-
|
|
58
|
-
toggleAutomation();
|
|
70
|
+
clearPromptForm();
|
|
59
71
|
fetchPipelines();
|
|
60
72
|
runPipeline(pipe.id);
|
|
61
73
|
} catch (err) {
|
|
@@ -94,13 +106,23 @@ async function sendTask(e) {
|
|
|
94
106
|
|
|
95
107
|
if (filePaths.length > 0) body.images = filePaths;
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
// 의존성 모드: depends_on 추가
|
|
110
|
+
if (_depsMode) {
|
|
111
|
+
const depsVal = document.getElementById('depsInput').value.trim();
|
|
112
|
+
if (depsVal) {
|
|
113
|
+
body.depends_on = depsVal.split(/[,\s]+/).filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await apiFetch('/api/send', { method: 'POST', body: JSON.stringify(body) });
|
|
98
118
|
const modeMsg = _contextMode === 'resume' ? ' (resume)' : _contextMode === 'fork' ? ' (fork)' : '';
|
|
99
|
-
|
|
119
|
+
const depMsg = result && result.status === 'pending' ? ' (대기 중 — 선행 작업 완료 후 실행)' : '';
|
|
120
|
+
showToast(t('msg_task_sent') + modeMsg + depMsg);
|
|
100
121
|
if (cwd) addRecentDir(cwd);
|
|
101
122
|
document.getElementById('promptInput').value = '';
|
|
102
123
|
clearAttachments();
|
|
103
124
|
clearContext();
|
|
125
|
+
if (_depsMode) toggleDeps();
|
|
104
126
|
fetchJobs();
|
|
105
127
|
} catch (err) {
|
|
106
128
|
showToast(`${t('msg_send_failed')}: ${err.message}`, 'error');
|