input-kanban 0.0.7 → 0.0.9
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/LICENSE +21 -0
- package/PROJECT_GUIDE.md +168 -6
- package/README.en.md +51 -5
- package/README.md +51 -5
- package/RELEASE_NOTES.md +19 -0
- package/bin/input-kanban.js +755 -29
- package/package.json +4 -3
- package/public/index.html +138 -20
- package/src/orchestrator.js +492 -175
- package/src/server.js +8 -4
- package/src/utils.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "input-kanban",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"input-kanban": "bin/input-kanban.js"
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"start": "node bin/input-kanban.js",
|
|
10
10
|
"check": "node --check bin/input-kanban.js && node --check bin/input-kanban-format-events.js && node --check bin/input-kanban-timestamp-events.js && node --check bin/input-kanban-tmux-overview.js && node --check src/server.js && node --check src/orchestrator.js && node --check src/appServerClient.js && node --check src/utils.js && node --check src/eventFormatter.js && node --check src/runners/index.js && node --check src/runners/headlessRunner.js && node --check src/runners/tmuxRunner.js && node --check src/runners/tmuxUtils.js && node --check src/tmux.js && node --test"
|
|
11
11
|
},
|
|
12
|
-
"dependencies": {},
|
|
13
12
|
"description": "A local Codex orchestration kanban dashboard",
|
|
13
|
+
"license": "MIT",
|
|
14
14
|
"files": [
|
|
15
15
|
"bin",
|
|
16
16
|
"src",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"README.en.md",
|
|
20
20
|
"RELEASE_NOTES.md",
|
|
21
21
|
"PROJECT_GUIDE.md",
|
|
22
|
-
"ENVIRONMENT.md"
|
|
22
|
+
"ENVIRONMENT.md",
|
|
23
|
+
"LICENSE"
|
|
23
24
|
],
|
|
24
25
|
"keywords": [
|
|
25
26
|
"codex",
|
package/public/index.html
CHANGED
|
@@ -84,11 +84,17 @@
|
|
|
84
84
|
.meta-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
85
85
|
.meta-chip.long .meta-value { max-width: min(680px, 72vw); }
|
|
86
86
|
.meta-chip .copy-btn { margin-left: 2px; }
|
|
87
|
+
.refresh-pulse-chip { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--line); border-radius: 999px; background: #020617; }
|
|
88
|
+
.refresh-pulse-dot { width: 12px; height: 12px; border: 2px solid #60a5fa; border-top-color: transparent; border-radius: 999px; opacity: .62; }
|
|
89
|
+
.refresh-pulse-chip.pulse .refresh-pulse-dot { animation: refresh-spin .8s ease-out; }
|
|
90
|
+
@keyframes refresh-spin { 0% { transform: rotate(0deg) scale(.75); opacity: 1; } 70% { transform: rotate(300deg) scale(1.18); opacity: 1; } 100% { transform: rotate(360deg) scale(1); opacity: .62; } }
|
|
87
91
|
.log-panel { margin-top: 16px; }
|
|
88
92
|
.file-tabs button { font-size: 13px; }
|
|
89
93
|
.copy-btn { padding: 2px 6px; margin: 0 0 0 6px; border-radius: 6px; font-size: 12px; line-height: 1.2; background: var(--gray); vertical-align: middle; }
|
|
90
94
|
.rename-btn { opacity: 0; pointer-events: none; transition: opacity .15s ease; }
|
|
91
95
|
.run-card:hover .rename-btn, .run-card:focus-within .rename-btn, .build-title:hover .rename-btn, .build-title:focus-within .rename-btn, .rename-btn:focus { opacity: 1; pointer-events: auto; }
|
|
96
|
+
.run-card-title-actions { display: inline-flex; align-items: center; gap: 3px; }
|
|
97
|
+
.archive-confirm-btn { min-width: 46px; padding: 4px 10px; border-color: rgba(248,113,113,.85); background: var(--red) !important; color: white; font-weight: 900; }
|
|
92
98
|
.icon-svg { width: 14px; height: 14px; display: block; }
|
|
93
99
|
.session-cell { word-break: break-all; }
|
|
94
100
|
.row-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
|
|
@@ -113,6 +119,7 @@
|
|
|
113
119
|
.modal-backdrop.hidden { display: none; }
|
|
114
120
|
.modal-card { width: min(760px, 100%); border: 1px solid var(--line); border-radius: 14px; background: var(--panel); box-shadow: 0 18px 60px rgba(0,0,0,.38); padding: 16px; }
|
|
115
121
|
.modal-card textarea { min-height: 220px; }
|
|
122
|
+
.page-footer { padding: 0 18px 18px; color: var(--muted); text-align: center; font-size: 12px; }
|
|
116
123
|
</style>
|
|
117
124
|
</head>
|
|
118
125
|
<body>
|
|
@@ -180,6 +187,7 @@
|
|
|
180
187
|
</section>
|
|
181
188
|
</div>
|
|
182
189
|
</main>
|
|
190
|
+
<footer id="pageFooter" class="page-footer">版本:-</footer>
|
|
183
191
|
<div id="manualCompleteModal" class="modal-backdrop hidden">
|
|
184
192
|
<div class="modal-card">
|
|
185
193
|
<h2>手动标记成功</h2>
|
|
@@ -198,12 +206,18 @@ let selectedRun = null;
|
|
|
198
206
|
let selectedTask = null;
|
|
199
207
|
let selectedFileName = null;
|
|
200
208
|
let manualCompleteTaskId = null;
|
|
209
|
+
let pendingArchiveRunId = null;
|
|
210
|
+
const autoDispatchingRuns = new Set();
|
|
211
|
+
const autoJudgingRuns = new Set();
|
|
212
|
+
const autoRetryingRuns = new Set();
|
|
213
|
+
const autoRetrySkippedRuns = new Set();
|
|
201
214
|
let currentState = null;
|
|
202
215
|
let lastAutoRefreshAt = null;
|
|
203
216
|
let runListVisibleCount = 10;
|
|
204
217
|
let latestRuns = [];
|
|
205
218
|
const statusByRunId = new Map();
|
|
206
219
|
const AUTO_REFRESH_MS = 3000;
|
|
220
|
+
const AUTO_MAX_RETRIES = 1;
|
|
207
221
|
const RUN_LIST_PAGE_SIZE = 10;
|
|
208
222
|
|
|
209
223
|
async function api(path, opts={}) {
|
|
@@ -226,6 +240,7 @@ function userFacingErrorMessage(error) {
|
|
|
226
240
|
if (/planner already running/i.test(detail)) return '任务拆分正在进行中,请稍后查看结果。';
|
|
227
241
|
if (/judge already running/i.test(detail)) return '验收正在进行中,请稍后查看结果。';
|
|
228
242
|
if (/already running/i.test(detail)) return '任务正在进行中,请稍后查看结果。';
|
|
243
|
+
if (/cannot archive.*running/i.test(detail)) return '任务仍在执行中,请先停止后再归档。';
|
|
229
244
|
return error?.message || String(error);
|
|
230
245
|
}
|
|
231
246
|
const statusText = {
|
|
@@ -279,6 +294,9 @@ function metaChip(label, value, { title = value, danger = false, long = false, e
|
|
|
279
294
|
function editIcon() {
|
|
280
295
|
return '<svg class="icon-svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true"><path d="M4 16.5V20h3.5L18.1 9.4l-3.5-3.5L4 16.5Z" fill="currentColor"/><path d="m16 4.5 3.5 3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
281
296
|
}
|
|
297
|
+
function archiveIcon() {
|
|
298
|
+
return '<svg class="icon-svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true"><path d="M4 5h16v4H4V5Z" fill="currentColor"/><path d="M6 10h12v9H6v-9Z" fill="currentColor" opacity=".72"/><path d="M9 13h6" stroke="#020617" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
299
|
+
}
|
|
282
300
|
function isTmuxMode() { return currentState?.runner === 'tmux'; }
|
|
283
301
|
function taskById(id) {
|
|
284
302
|
if (!currentState) return null;
|
|
@@ -319,6 +337,7 @@ async function loadHealth() {
|
|
|
319
337
|
const h = await api('/api/health');
|
|
320
338
|
document.getElementById('repo').value = h.defaultRepo;
|
|
321
339
|
document.getElementById('runsDir').value = h.runsDir;
|
|
340
|
+
document.getElementById('pageFooter').textContent = h.version ? `版本:v${h.version}` : '版本:未知(请重启服务)';
|
|
322
341
|
}
|
|
323
342
|
function showCreateForm() {
|
|
324
343
|
selectedRun = null; selectedTask = null; selectedFileName = null; currentState = null;
|
|
@@ -346,12 +365,13 @@ async function refreshRuns() {
|
|
|
346
365
|
const data = await api('/api/runs');
|
|
347
366
|
latestRuns = data.runs || [];
|
|
348
367
|
renderRunList();
|
|
368
|
+
await maybeAutoAdvanceRunSummaries(latestRuns);
|
|
349
369
|
}
|
|
350
370
|
function renderRunList() {
|
|
351
371
|
const visibleRuns = latestRuns.slice(0, runListVisibleCount);
|
|
352
372
|
const cards = visibleRuns.map(r => `
|
|
353
|
-
<div class="run-card ${selectedRun === r.runId ? 'active' : ''}" onclick="selectRun('${r.runId}')">
|
|
354
|
-
<div class="run-card-title"><span class="run-card-name-wrap"><span class="run-card-name" title="${esc(r.label)}">${esc(r.label)}</span><button class="secondary copy-btn rename-btn" title="修改任务批次名称" onclick="renameRunLabel(event, '${r.runId}')">${editIcon()}</button></span><span>${pill(r.status)}</span></div>
|
|
373
|
+
<div class="run-card ${selectedRun === r.runId ? 'active' : ''}" onclick="selectRun('${r.runId}')" onmouseleave="clearArchiveConfirm('${r.runId}')">
|
|
374
|
+
<div class="run-card-title"><span class="run-card-name-wrap"><span class="run-card-name" title="${esc(r.label)}">${esc(r.label)}</span><span class="run-card-title-actions"><button class="secondary copy-btn rename-btn" title="修改任务批次名称" onclick="renameRunLabel(event, '${r.runId}')">${editIcon()}</button><button class="secondary copy-btn rename-btn ${pendingArchiveRunId === r.runId ? 'archive-confirm-btn' : ''}" title="${pendingArchiveRunId === r.runId ? '再次点击确认归档' : '归档任务批次(运行中请先停止)'}" onclick="archiveRunFromCard(event, '${r.runId}')">${pendingArchiveRunId === r.runId ? '确认' : archiveIcon()}</button></span></span><span>${pill(r.status)}</span></div>
|
|
355
375
|
<div class="run-card-meta">
|
|
356
376
|
${metaChip('仓库', basenamePath(r.repo), { title: r.repo, long: true, extra: `<button class="secondary copy-btn" title="复制仓库地址" onclick="copyRunRepoPath(event, '${r.runId}')">⧉</button>` })}
|
|
357
377
|
${metaChip('创建', formatDateTime(r.createdAt))}
|
|
@@ -371,17 +391,19 @@ function showMoreRuns() {
|
|
|
371
391
|
renderRunList();
|
|
372
392
|
}
|
|
373
393
|
async function selectRun(id) { selectedRun = id; selectedTask = null; selectedFileName = null; clearFileView(); hideCreateForm(); await refreshSelected(); }
|
|
374
|
-
async function refreshSelected({auto=false} = {}) {
|
|
394
|
+
async function refreshSelected({auto=false, skipAutoAdvance=false} = {}) {
|
|
375
395
|
if (!selectedRun) return;
|
|
376
396
|
currentState = await api(`/api/runs/${selectedRun}/status`);
|
|
377
397
|
statusByRunId.set(selectedRun, currentState);
|
|
378
398
|
if (auto) lastAutoRefreshAt = new Date();
|
|
379
399
|
document.getElementById('selected').innerHTML = renderSelectedHeader();
|
|
400
|
+
if (auto) requestAnimationFrame(triggerRefreshPulse);
|
|
380
401
|
updateAutoRefreshHint();
|
|
381
402
|
updateRunNotice();
|
|
382
403
|
await loadTaskDescription();
|
|
383
404
|
renderTasks(); await refreshRuns();
|
|
384
405
|
if (selectedTask && selectedFileName) await loadFile(selectedFileName, { preserveScroll: true });
|
|
406
|
+
if (!skipAutoAdvance) await maybeAutoAdvanceSelectedRun();
|
|
385
407
|
}
|
|
386
408
|
function renderSelectedHeader() {
|
|
387
409
|
if (!currentState) return '<div class="muted">未选择任务批次</div>';
|
|
@@ -407,14 +429,24 @@ function renderSelectedHeader() {
|
|
|
407
429
|
chips.push(metaChip('终端', 'tmux 现场尚未生成'));
|
|
408
430
|
}
|
|
409
431
|
}
|
|
410
|
-
chips.push(
|
|
411
|
-
chips.push(metaChip('上次', lastAutoRefreshAt ? lastAutoRefreshAt.toLocaleTimeString() : '尚未触发'));
|
|
432
|
+
chips.push(refreshPulseChip());
|
|
412
433
|
return `<div class="build-title"><span>${esc(currentState.label)}</span><button class="secondary copy-btn rename-btn" title="修改任务批次名称" onclick="renameRunLabel(event, currentState.runId)">${editIcon()}</button>${pill(currentState.status)}</div><div class="build-meta">${chips.join('')}</div>`;
|
|
413
434
|
}
|
|
414
435
|
async function loadTaskDescription() {
|
|
415
436
|
if (!selectedRun) { document.getElementById('taskDescription').textContent = '未选择任务批次'; return; }
|
|
416
437
|
document.getElementById('taskDescription').textContent = await api(`/api/runs/${selectedRun}/task-text`);
|
|
417
438
|
}
|
|
439
|
+
function refreshPulseChip() {
|
|
440
|
+
const last = lastAutoRefreshAt ? lastAutoRefreshAt.toLocaleTimeString() : '尚未触发';
|
|
441
|
+
return `<span id="refreshPulse" class="refresh-pulse-chip" title="自动刷新:每 ${AUTO_REFRESH_MS / 1000} 秒;上次 ${esc(last)}"><span class="refresh-pulse-dot"></span></span>`;
|
|
442
|
+
}
|
|
443
|
+
function triggerRefreshPulse() {
|
|
444
|
+
const el = document.getElementById('refreshPulse');
|
|
445
|
+
if (!el) return;
|
|
446
|
+
el.classList.remove('pulse');
|
|
447
|
+
void el.offsetWidth;
|
|
448
|
+
el.classList.add('pulse');
|
|
449
|
+
}
|
|
418
450
|
function updateRunNotice() {
|
|
419
451
|
const el = document.getElementById('runNotice');
|
|
420
452
|
if (!el) return;
|
|
@@ -441,9 +473,9 @@ function updateAutoRefreshHint() {
|
|
|
441
473
|
const el = document.getElementById('autoRefreshHint');
|
|
442
474
|
if (!el) return;
|
|
443
475
|
if (!selectedRun || !currentState) { el.textContent = '自动刷新:未启动'; return; }
|
|
444
|
-
const active = ['planning','running','judging'].includes(currentState.status) || (currentState.tasks || []).some(t => t.status === 'running') || currentState.planner?.status === 'running' || currentState.judge?.status === 'running';
|
|
476
|
+
const active = ['planning','running','judging','planned','batches_completed','batch_blocked'].includes(currentState.status) || (currentState.tasks || []).some(t => t.status === 'running') || currentState.planner?.status === 'running' || currentState.judge?.status === 'running';
|
|
445
477
|
const last = lastAutoRefreshAt ? lastAutoRefreshAt.toLocaleTimeString() : '尚未触发';
|
|
446
|
-
el.textContent = active ?
|
|
478
|
+
el.textContent = active ? `自动模式中:每 ${AUTO_REFRESH_MS / 1000} 秒刷新并推进一次|上次刷新 ${last}` : `自动模式待命:每 ${AUTO_REFRESH_MS / 1000} 秒检查一次|上次刷新 ${last}`;
|
|
447
479
|
}
|
|
448
480
|
function taskStatusCell(t) {
|
|
449
481
|
if (t?.manualCompletion) {
|
|
@@ -780,6 +812,67 @@ async function renameRunLabel(event, runId = selectedRun) {
|
|
|
780
812
|
else await refreshRuns();
|
|
781
813
|
});
|
|
782
814
|
}
|
|
815
|
+
async function maybeAutoAdvanceRunSummaries(runs) {
|
|
816
|
+
for (const run of runs || []) {
|
|
817
|
+
if (!run || run.runId === selectedRun) continue;
|
|
818
|
+
if (run.status !== 'batch_blocked') autoRetrySkippedRuns.delete(run.runId);
|
|
819
|
+
if (run.status === 'planned') await autoDispatchRun(run.runId, false);
|
|
820
|
+
else if (run.status === 'batches_completed') await autoJudgeRun(run.runId, false);
|
|
821
|
+
else if (run.status === 'batch_blocked') await autoRetryRun(run.runId, false);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function autoDispatchRun(runId, refreshSelectedAfter = true) {
|
|
825
|
+
if (!runId || autoDispatchingRuns.has(runId)) return false;
|
|
826
|
+
autoDispatchingRuns.add(runId);
|
|
827
|
+
try {
|
|
828
|
+
await api(`/api/runs/${runId}/dispatch`, {method:'POST'});
|
|
829
|
+
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
830
|
+
return true;
|
|
831
|
+
} catch (error) {
|
|
832
|
+
console.error('自动派发失败', error);
|
|
833
|
+
return false;
|
|
834
|
+
} finally {
|
|
835
|
+
autoDispatchingRuns.delete(runId);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function autoJudgeRun(runId, refreshSelectedAfter = true) {
|
|
839
|
+
if (!runId || autoJudgingRuns.has(runId)) return false;
|
|
840
|
+
autoJudgingRuns.add(runId);
|
|
841
|
+
try {
|
|
842
|
+
await api(`/api/runs/${runId}/judge`, {method:'POST'});
|
|
843
|
+
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
844
|
+
return true;
|
|
845
|
+
} catch (error) {
|
|
846
|
+
console.error('自动验收失败', error);
|
|
847
|
+
return false;
|
|
848
|
+
} finally {
|
|
849
|
+
autoJudgingRuns.delete(runId);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function autoRetryRun(runId, refreshSelectedAfter = true) {
|
|
853
|
+
if (!runId || autoRetryingRuns.has(runId) || autoRetrySkippedRuns.has(runId)) return false;
|
|
854
|
+
autoRetryingRuns.add(runId);
|
|
855
|
+
try {
|
|
856
|
+
await api(`/api/runs/${runId}/retry`, {method:'POST', body: JSON.stringify({ reason: 'auto retry from dashboard', maxRetries: AUTO_MAX_RETRIES, auto: true })});
|
|
857
|
+
if (refreshSelectedAfter && selectedRun === runId) await refreshSelected({auto:true, skipAutoAdvance:true});
|
|
858
|
+
return true;
|
|
859
|
+
} catch (error) {
|
|
860
|
+
autoRetrySkippedRuns.add(runId);
|
|
861
|
+
console.error('自动重试失败', error);
|
|
862
|
+
return false;
|
|
863
|
+
} finally {
|
|
864
|
+
autoRetryingRuns.delete(runId);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function maybeAutoAdvanceSelectedRun() {
|
|
868
|
+
const runId = selectedRun;
|
|
869
|
+
const state = currentState;
|
|
870
|
+
if (!runId || !state) return;
|
|
871
|
+
if (state.status !== 'batch_blocked') autoRetrySkippedRuns.delete(runId);
|
|
872
|
+
if (state.status === 'planned') await autoDispatchRun(runId);
|
|
873
|
+
else if (state.status === 'batches_completed' && state.judge?.status !== 'running' && state.judge?.status !== 'completed') await autoJudgeRun(runId);
|
|
874
|
+
else if (state.status === 'batch_blocked') await autoRetryRun(runId);
|
|
875
|
+
}
|
|
783
876
|
async function planRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/plan`, {method:'POST'}); await refreshSelected(); }); }
|
|
784
877
|
async function dispatchRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/dispatch`, {method:'POST'}); await refreshSelected(); }); }
|
|
785
878
|
async function judgeRun() { if (selectedRun) await runAction(async () => { await api(`/api/runs/${selectedRun}/judge`, {method:'POST'}); await refreshSelected(); }); }
|
|
@@ -800,21 +893,46 @@ async function stopSelectedRun() {
|
|
|
800
893
|
});
|
|
801
894
|
await refreshSelected();
|
|
802
895
|
}
|
|
896
|
+
function clearArchiveConfirm(runId) {
|
|
897
|
+
if (pendingArchiveRunId !== runId) return;
|
|
898
|
+
pendingArchiveRunId = null;
|
|
899
|
+
renderRunList();
|
|
900
|
+
}
|
|
901
|
+
async function archiveRunFromCard(event, runId) {
|
|
902
|
+
event.stopPropagation();
|
|
903
|
+
if (pendingArchiveRunId !== runId) {
|
|
904
|
+
pendingArchiveRunId = runId;
|
|
905
|
+
renderRunList();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
pendingArchiveRunId = null;
|
|
909
|
+
await archiveRunById(runId, { confirmFirst: false });
|
|
910
|
+
}
|
|
911
|
+
async function archiveRunById(runId, { confirmFirst = true } = {}) {
|
|
912
|
+
if (!runId) return;
|
|
913
|
+
if (confirmFirst) {
|
|
914
|
+
const ok = confirm('确认归档当前任务批次?\n\n归档后会从默认任务批次列表隐藏。若仍有任务运行,请先停止。');
|
|
915
|
+
if (!ok) return;
|
|
916
|
+
}
|
|
917
|
+
await runAction(async () => {
|
|
918
|
+
await api(`/api/runs/${runId}/archive`, {
|
|
919
|
+
method: 'POST',
|
|
920
|
+
body: JSON.stringify({ reason: 'archived from dashboard' })
|
|
921
|
+
});
|
|
922
|
+
if (selectedRun === runId) {
|
|
923
|
+
selectedRun = null; selectedTask = null; selectedFileName = null; currentState = null;
|
|
924
|
+
document.getElementById('selected').textContent = '未选择任务批次';
|
|
925
|
+
document.getElementById('taskDescription').textContent = '未选择任务批次';
|
|
926
|
+
document.getElementById('tasks').innerHTML = '';
|
|
927
|
+
clearFileView();
|
|
928
|
+
updateAutoRefreshHint();
|
|
929
|
+
}
|
|
930
|
+
await refreshRuns();
|
|
931
|
+
});
|
|
932
|
+
}
|
|
803
933
|
async function archiveSelectedRun() {
|
|
804
934
|
if (!selectedRun) return;
|
|
805
|
-
|
|
806
|
-
if (!ok) return;
|
|
807
|
-
await api(`/api/runs/${selectedRun}/archive`, {
|
|
808
|
-
method: 'POST',
|
|
809
|
-
body: JSON.stringify({ reason: 'archived from dashboard' })
|
|
810
|
-
});
|
|
811
|
-
selectedRun = null; selectedTask = null; selectedFileName = null; currentState = null;
|
|
812
|
-
document.getElementById('selected').textContent = '未选择任务批次';
|
|
813
|
-
document.getElementById('taskDescription').textContent = '未选择任务批次';
|
|
814
|
-
document.getElementById('tasks').innerHTML = '';
|
|
815
|
-
clearFileView();
|
|
816
|
-
updateAutoRefreshHint();
|
|
817
|
-
await refreshRuns();
|
|
935
|
+
await archiveRunById(selectedRun);
|
|
818
936
|
}
|
|
819
937
|
async function markTaskCompleted(event, taskId) {
|
|
820
938
|
event.stopPropagation();
|