claude-controller 0.1.2 → 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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +1189 -0
  4. package/bin/native-app.py +6 -3
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +11 -5
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +634 -473
  38. package/web/handler_fs.py +153 -0
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +372 -0
  41. package/web/handler_memory.py +203 -0
  42. package/web/handler_sessions.py +132 -0
  43. package/web/jobs.py +585 -13
  44. package/web/personas.py +419 -0
  45. package/web/pipeline.py +981 -0
  46. package/web/presets.py +506 -0
  47. package/web/projects.py +246 -0
  48. package/web/static/api.js +141 -0
  49. package/web/static/app.js +25 -1937
  50. package/web/static/attachments.js +144 -0
  51. package/web/static/base.css +497 -0
  52. package/web/static/context.js +204 -0
  53. package/web/static/dirs.js +246 -0
  54. package/web/static/form.css +763 -0
  55. package/web/static/goals.css +363 -0
  56. package/web/static/goals.js +300 -0
  57. package/web/static/i18n.js +625 -0
  58. package/web/static/index.html +215 -13
  59. package/web/static/{styles.css → jobs.css} +746 -1141
  60. package/web/static/jobs.js +1270 -0
  61. package/web/static/memoryview.js +117 -0
  62. package/web/static/personas.js +228 -0
  63. package/web/static/pipeline.css +338 -0
  64. package/web/static/pipelines.js +487 -0
  65. package/web/static/presets.js +244 -0
  66. package/web/static/send.js +135 -0
  67. package/web/static/settings-style.css +291 -0
  68. package/web/static/settings.js +81 -0
  69. package/web/static/stream.js +534 -0
  70. package/web/static/utils.js +131 -0
  71. package/web/webhook.py +210 -0
@@ -6,10 +6,20 @@
6
6
  <title data-i18n="title">Controller Service</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
- <link rel="stylesheet" href="styles.css">
9
+ <link rel="stylesheet" href="base.css">
10
+ <link rel="stylesheet" href="form.css">
11
+ <link rel="stylesheet" href="jobs.css">
12
+ <link rel="stylesheet" href="settings-style.css">
13
+ <link rel="stylesheet" href="pipeline.css">
10
14
  </head>
11
15
  <body>
12
16
 
17
+ <!-- Connection lost banner -->
18
+ <div class="conn-lost-banner" id="connLostBanner">
19
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1l22 22"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.56 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
20
+ <span data-i18n="conn_lost">서버 연결이 끊어졌습니다. 재연결 시도 중...</span>
21
+ </div>
22
+
13
23
  <!-- ── Layout ── -->
14
24
  <div class="layout">
15
25
 
@@ -58,7 +68,12 @@
58
68
  <div class="recent-dirs" id="recentDirs"></div>
59
69
 
60
70
  <div class="form-group">
61
- <label class="form-label"><span data-i18n="prompt">프롬프트</span> <span class="img-count-badge" id="imgCountBadge"></span></label>
71
+ <label class="form-label" style="display:flex;align-items:center;gap:6px;">
72
+ <span data-i18n="prompt">프롬프트</span>
73
+ <span class="img-count-badge" id="imgCountBadge"></span>
74
+ <span id="personaBadge" style="display:none;"></span>
75
+ <span id="promptSessionInfo" style="margin-left:auto;font-size:0.68rem;font-weight:400;color:var(--text-muted);font-family:var(--font-mono,monospace);"></span>
76
+ </label>
62
77
  <div class="prompt-wrapper" id="promptWrapper">
63
78
  <textarea id="promptInput" data-i18n-placeholder="prompt_placeholder" placeholder="Claude에게 전달할 명령을 입력하세요... (파일/이미지 드래그 또는 붙여넣기 가능)" rows="2"></textarea>
64
79
  <div class="prompt-mirror" id="promptMirror"></div>
@@ -91,8 +106,20 @@
91
106
  <div class="dir-browser-panel" id="dirBrowserPanel">
92
107
  <div class="dir-modal-header">
93
108
  <span class="dir-modal-title" data-i18n="browse_directory">디렉토리 탐색</span>
94
- <button type="button" class="dir-modal-close" onclick="closeDirBrowser()">
95
- <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>
109
+ <div style="display:flex;align-items:center;gap:4px;">
110
+ <button type="button" class="dir-modal-close" onclick="showCreateDirInput()" title=" 폴더 만들기">
111
+ <svg width="16" height="16" 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"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
112
+ </button>
113
+ <button type="button" class="dir-modal-close" onclick="closeDirBrowser()">
114
+ <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>
115
+ </button>
116
+ </div>
117
+ </div>
118
+ <div class="dir-create-row" id="dirCreateRow" style="display:none;">
119
+ <input type="text" id="dirCreateInput" class="dir-create-input" placeholder="새 폴더 이름" onkeydown="if(event.key==='Enter')createDir();if(event.key==='Escape')hideCreateDirInput();">
120
+ <button type="button" class="btn btn-sm btn-primary" onclick="createDir()" data-i18n="create">생성</button>
121
+ <button type="button" class="btn btn-sm" onclick="hideCreateDirInput()">
122
+ <svg width="12" height="12" 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>
96
123
  </button>
97
124
  </div>
98
125
  <div class="dir-modal-path" id="dirBreadcrumb"></div>
@@ -105,11 +132,31 @@
105
132
  </div>
106
133
 
107
134
  <div class="form-actions">
108
- <button type="button" class="btn" onclick="clearPromptForm();" data-i18n="reset">초기화</button>
109
- <button type="submit" class="btn btn-primary" id="btnSend">
110
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
111
- <span data-i18n="send">전송</span>
112
- </button>
135
+ <div class="automation-inline" id="automationRow" style="display:none;">
136
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
137
+ <span class="automation-inline-label">반복 간격</span>
138
+ <input type="text" class="setting-input" id="automationInterval" placeholder="5m" style="width:60px;padding:3px 8px;font-size:0.72rem;">
139
+ <span class="automation-inline-hint">예: 30s, 5m, 1h</span>
140
+ </div>
141
+ <!-- DAG 의존성 입력: AI 전용 (API depends_on), 프론트 UI 숨김 -->
142
+ <div class="automation-inline" id="depsRow" style="display:none;">
143
+ <input type="hidden" id="depsInput">
144
+ </div>
145
+ <div class="form-actions-buttons">
146
+ <button type="button" class="btn" onclick="clearPromptForm();" data-i18n="reset">초기화</button>
147
+ <!-- DAG 버튼 숨김: AI가 API로 depends_on을 직접 사용 -->
148
+ <button type="button" class="btn" id="btnDepsToggle" onclick="toggleDeps()" style="display:none;">
149
+ <span>DAG</span>
150
+ </button>
151
+ <button type="button" class="btn" id="btnAutoToggle" onclick="toggleAutomation()" title="자동화 모드 전환" style="display:flex;align-items:center;gap:4px;">
152
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
153
+ <span>자동화</span>
154
+ </button>
155
+ <button type="submit" class="btn btn-primary" id="btnSend">
156
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
157
+ <span data-i18n="send">전송</span>
158
+ </button>
159
+ </div>
113
160
  </div>
114
161
  </form>
115
162
  </div>
@@ -130,12 +177,51 @@
130
177
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
131
178
  <span data-i18n="delete_completed">완료 삭제</span>
132
179
  </button>
180
+ <button class="btn btn-sm btn-icon" id="btnViewMode" onclick="toggleJobViewMode()" title="프로젝트별 그룹 보기">
181
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
182
+ </button>
133
183
  <button class="btn btn-sm btn-icon" onclick="fetchJobs()" title="새로고침">
134
184
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
135
185
  </button>
186
+ <button class="btn btn-sm btn-icon btn-collapse" id="btnCollapseJobs" onclick="toggleJobListCollapse()" title="최소화">
187
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
188
+ </button>
189
+ </div>
190
+ </div>
191
+ <div class="project-strip" id="projectStrip"></div>
192
+ <div class="project-detail" id="projectDetail" style="display:none;"></div>
193
+ <div class="stats-bar" id="statsBar">
194
+ <div class="stats-period-btns">
195
+ <button class="stats-period-btn active" data-period="all" onclick="setStatsPeriod('all')">전체</button>
196
+ <button class="stats-period-btn" data-period="day" onclick="setStatsPeriod('day')">24h</button>
197
+ <button class="stats-period-btn" data-period="week" onclick="setStatsPeriod('week')">7d</button>
198
+ <button class="stats-period-btn" data-period="month" onclick="setStatsPeriod('month')">30d</button>
199
+ </div>
200
+ <div class="stats-metrics" id="statsMetrics">
201
+ <span class="stats-metric" id="statTotal" title="전체 작업 수">-</span>
202
+ <span class="stats-metric stats-success" id="statSuccess" title="성공률">-</span>
203
+ <span class="stats-metric stats-duration" id="statDuration" title="평균 소요시간">-</span>
204
+ </div>
205
+ </div>
206
+ <div class="job-filter-bar" id="jobFilterBar">
207
+ <div class="job-filter-btns">
208
+ <button class="job-filter-btn active" data-filter="all" onclick="setJobFilter('all')">전체</button>
209
+ <button class="job-filter-btn" data-filter="running" onclick="setJobFilter('running')">실행 중</button>
210
+ <button class="job-filter-btn" data-filter="done" onclick="setJobFilter('done')">완료</button>
211
+ <button class="job-filter-btn" data-filter="failed" onclick="setJobFilter('failed')">실패</button>
212
+ </div>
213
+ <div class="job-project-filter">
214
+ <svg width="13" height="13" 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>
215
+ <select id="jobProjectSelect" onchange="setJobProjectFilter(this.value)">
216
+ <option value="all" data-i18n="all_projects">전체 프로젝트</option>
217
+ </select>
218
+ </div>
219
+ <div class="job-search-wrap">
220
+ <svg width="13" height="13" 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>
221
+ <input type="text" class="job-search-input" id="jobSearchInput" placeholder="프롬프트 검색..." oninput="applyJobFilters()">
136
222
  </div>
137
223
  </div>
138
- <div class="table-wrap">
224
+ <div class="table-wrap" id="jobTableWrap">
139
225
  <table>
140
226
  <thead>
141
227
  <tr>
@@ -144,16 +230,70 @@
144
230
  <th data-i18n="prompt">프롬프트</th>
145
231
  <th style="width:12%;" data-i18n="folder">폴더</th>
146
232
  <th style="width:9%;">Session</th>
147
- <th style="width:14%;" data-i18n="created_at">생성 시간</th>
148
233
  <th style="width:10%;"></th>
149
234
  </tr>
150
235
  </thead>
151
236
  <tbody id="jobTableBody">
152
- <tr data-job-id="__loading__"><td colspan="7" class="empty-state">
237
+ <tr data-job-id="__loading__"><td colspan="6" class="empty-state">
153
238
  <div data-i18n="loading_jobs">작업 목록을 불러오는 중...</div>
154
239
  </td></tr>
155
240
  </tbody>
156
241
  </table>
242
+ <div id="jobPagination"></div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Section 3: Personas -->
248
+ <div class="section" id="personaSection">
249
+ <div class="card">
250
+ <div class="card-header">
251
+ <div class="card-title">
252
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
253
+ <span data-i18n="personas">페르소나</span>
254
+ </div>
255
+ <div class="card-header-actions">
256
+ <button class="btn btn-sm" onclick="openCreatePersonaDialog()" title="커스텀 페르소나 만들기">
257
+ <svg width="12" height="12" 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>
258
+ <span>만들기</span>
259
+ </button>
260
+ <button class="btn btn-sm btn-icon" onclick="fetchPersonas()" title="새로고침">
261
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
262
+ </button>
263
+ </div>
264
+ </div>
265
+ <div id="personaGrid" class="persona-grid">
266
+ <div class="empty-state" style="padding:20px; text-align:center; color:var(--text-muted); font-size:0.8rem;">
267
+ 페르소나를 불러오는 중...
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Section 4: Automations -->
274
+ <div class="section" id="pipelineSection">
275
+ <div class="card">
276
+ <div class="card-header">
277
+ <div class="card-title">
278
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
279
+ <span data-i18n="automations">자동화</span>
280
+ <span id="pipelineCount" style="font-size:0.72rem; color:var(--text-muted); font-weight:400;"></span>
281
+ </div>
282
+ <div class="card-header-actions">
283
+ <button class="btn btn-sm btn-primary" onclick="openCreatePipeline()" title="새 자동화 생성">
284
+ <svg width="14" height="14" 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>
285
+ <span style="margin-left:2px;">새 자동화</span>
286
+ </button>
287
+ <button class="btn btn-sm btn-icon" onclick="fetchPipelines()" title="새로고침">
288
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
289
+ </button>
290
+ </div>
291
+ </div>
292
+ <div id="evolutionSummary"></div>
293
+ <div id="pipelineList" class="pipeline-list">
294
+ <div class="empty-state" style="padding:20px; text-align:center; color:var(--text-muted); font-size:0.8rem;">
295
+ 자동화가 없습니다. 위 프리셋을 적용하거나 입력창에서 자동화를 등록하세요.
296
+ </div>
157
297
  </div>
158
298
  </div>
159
299
  </div>
@@ -161,6 +301,14 @@
161
301
  </main>
162
302
  </div>
163
303
 
304
+ <!-- Pipeline modal removed — 자동화 등록은 입력창에서 직접 수행 -->
305
+
306
+ <!-- Theme Toggle FAB -->
307
+ <button class="theme-fab" onclick="toggleTheme()" title="테마 전환">
308
+ <svg class="theme-icon-dark" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
309
+ <svg class="theme-icon-light" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
310
+ </button>
311
+
164
312
  <!-- Settings FAB -->
165
313
  <button class="settings-fab" onclick="openSettings()" title="설정">
166
314
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
@@ -203,6 +351,54 @@
203
351
  </div>
204
352
  </div>
205
353
 
354
+ <!-- 웹훅 / Webhook -->
355
+ <div class="settings-section">
356
+ <div class="settings-section-title" data-i18n="webhook_settings">웹훅 설정</div>
357
+ <div class="setting-row">
358
+ <div class="setting-info">
359
+ <div class="setting-label" data-i18n="webhook_url">Webhook URL</div>
360
+ <div class="setting-desc" data-i18n="webhook_url_desc">작업 완료/실패 시 결과를 POST할 URL</div>
361
+ </div>
362
+ <div class="setting-control">
363
+ <input type="url" class="setting-input" id="cfgWebhookUrl" placeholder="https://example.com/webhook" style="width:260px;">
364
+ </div>
365
+ </div>
366
+ <div class="setting-row">
367
+ <div class="setting-info">
368
+ <div class="setting-label" data-i18n="webhook_secret">Webhook Secret</div>
369
+ <div class="setting-desc" data-i18n="webhook_secret_desc">HMAC-SHA256 서명용 비밀 키 (선택사항)</div>
370
+ </div>
371
+ <div class="setting-control">
372
+ <input type="password" class="setting-input" id="cfgWebhookSecret" placeholder="선택사항" style="width:260px;">
373
+ </div>
374
+ </div>
375
+ <div class="setting-row">
376
+ <div class="setting-info">
377
+ <div class="setting-label" data-i18n="webhook_events">이벤트 필터</div>
378
+ <div class="setting-desc" data-i18n="webhook_events_desc">웹훅을 보낼 이벤트 (done, failed)</div>
379
+ </div>
380
+ <div class="setting-control">
381
+ <select class="setting-input" id="cfgWebhookEvents" style="width:160px;">
382
+ <option value="done,failed">전체 (done, failed)</option>
383
+ <option value="done">완료만 (done)</option>
384
+ <option value="failed">실패만 (failed)</option>
385
+ </select>
386
+ </div>
387
+ </div>
388
+ <div class="setting-row">
389
+ <div class="setting-info">
390
+ <div class="setting-label" data-i18n="webhook_test">연결 테스트</div>
391
+ <div class="setting-desc" data-i18n="webhook_test_desc">설정된 URL로 테스트 이벤트를 전송합니다</div>
392
+ </div>
393
+ <div class="setting-control">
394
+ <button class="btn btn-sm" id="btnWebhookTest" onclick="testWebhook()">
395
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
396
+ 테스트 전송
397
+ </button>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
206
402
  </div>
207
403
  <div class="settings-footer">
208
404
  <button class="btn" onclick="closeSettings()" data-i18n="close">닫기</button>
@@ -211,9 +407,15 @@
211
407
  </div>
212
408
  </div>
213
409
 
410
+ <!-- Project Modal -->
411
+
214
412
  <!-- Toast Container -->
215
413
  <div class="toast-container" id="toastContainer"></div>
216
414
 
217
- <script src="app.js"></script>
415
+ <script>
416
+ // 캐시 버스팅: 순서 보장 로드
417
+ ['i18n','utils','api','context','attachments','dirs','send','stream','jobs','pipelines','personas','settings','app']
418
+ .forEach(m => { const s = document.createElement('script'); s.src = m + '.js?t=' + Date.now(); s.async = false; document.body.appendChild(s); });
419
+ </script>
218
420
  </body>
219
421
  </html>